commit e936b525dfa4a3b5c04afb51d0caeffed90b5d9b Author: Afdholanabil Date: Fri Jun 20 11:51:16 2025 +0700 initialize commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..105d4c4 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +App_Skripsi \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..30bab2a --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..866ba02 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..6d0ee1c --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..0312e8a --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,714 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.kotlin/errors/errors-1742292039483.log b/.kotlin/errors/errors-1742292039483.log new file mode 100644 index 0000000..10065ff --- /dev/null +++ b/.kotlin/errors/errors-1742292039483.log @@ -0,0 +1,46 @@ +kotlin version: 2.0.0 +error message: java.lang.InterruptedException + +error message: Daemon compilation failed: Could not connect to Kotlin compile daemon +java.lang.RuntimeException: Could not connect to Kotlin compile daemon + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) + at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76) + at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) + at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:200) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:195) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53) + at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73) + at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) + at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:187) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:120) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:162) + at org.gradle.internal.Factories$1.create(Factories.java:31) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:264) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:128) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:133) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:157) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:126) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:47) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) + at java.base/java.lang.Thread.run(Thread.java:840) + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..3a92178 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,99 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + id("com.google.gms.google-services") + id("kotlin-parcelize") + id("kotlin-kapt") + id("com.google.devtools.ksp") +} + +android { + namespace = "com.example.app_skripsi" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.app_skripsi" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + +// sp abi { +// isEnable = true +// reset() +// include("armeabi-v7a", "arm64-v8a", "x86", "x86_64") +// isUniversalApk = true +// } +// density { isEnable = false } +//}lits { + + + buildTypes { + release { + isMinifyEnabled = 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 { + viewBinding = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.firebase.auth.ktx) + implementation(libs.firebase.firestore.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + implementation (libs.kotlinx.coroutines.play.services) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.fragment.ktx) + implementation (libs.androidx.datastore.preferences) + implementation (libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) // Pastikan compiler ada + implementation (libs.androidx.room.ktx) // Coroutine Support + implementation (libs.androidx.work.runtime) + implementation(libs.androidx.work.runtime.ktx) + + // Media3 (ExoPlayer) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.ui) + + // Untuk mengambil gambar thumbnail + implementation(libs.glide) + annotationProcessor(libs.compiler) + + implementation (libs.gson) + + implementation (libs.rive.android) + + implementation (libs.androidx.startup.runtime) + +// implementation (libs.mpandroidchart) + +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..ad3a199 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,30 @@ +{ + "project_info": { + "project_number": "469315857846", + "firebase_url": "https://appskripsi-bacc1-default-rtdb.asia-southeast1.firebasedatabase.app", + "project_id": "appskripsi-bacc1", + "storage_bucket": "appskripsi-bacc1.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:469315857846:android:2f42ccdf7b912aa44c6517", + "android_client_info": { + "package_name": "com.example.app_skripsi" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCTN8YGJMFBakt3gLbOt3feGs5BNrrj1gI" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..34afd9f --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# 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 + +#-keepclassmembers class com.example.app_skripsi.data.model.UserModel { +# public (); +#} diff --git a/app/src/androidTest/java/com/example/app_skripsi/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/app_skripsi/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..8c620d7 --- /dev/null +++ b/app/src/androidTest/java/com/example/app_skripsi/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.app_skripsi + +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.app_skripsi", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9e8f254 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/TrainingData.json b/app/src/main/assets/TrainingData.json new file mode 100644 index 0000000..03ba308 --- /dev/null +++ b/app/src/main/assets/TrainingData.json @@ -0,0 +1,5301 @@ +[ + { + "emosi":2, + "aktivitas":3, + "hari":2, + "gadScore":13, + "label":"moderate" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":15, + "label":"parah" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":10, + "label":"moderate" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":11, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":11, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":14, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":14, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":14, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":14, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":10, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":13, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":13, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":13, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":11, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":10, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":2, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":10, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":4, + "aktivitas":1, + "hari":1, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":4, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":3, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":1, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":4, + "aktivitas":1, + "hari":4, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":2, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":3, + "hari":3, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":3, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":1, + "hari":4, + "gadScore":13, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":6, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":0, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":4, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":3, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":1, + "hari":3, + "gadScore":15, + "label":"parah" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":11, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":4, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":4, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":3, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":14, + "label":"moderate" + }, + { + "emosi":3, + "aktivitas":3, + "hari":6, + "gadScore":15, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":1, + "hari":1, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":5, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":0, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":2, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":6, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":3, + "hari":0, + "gadScore":11, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":4, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":4, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":4, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":4, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":4, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":4, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":3, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":4, + "aktivitas":0, + "hari":4, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":3, + "aktivitas":1, + "hari":6, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":3, + "aktivitas":1, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":0, + "hari":0, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":2, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":2, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":2, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":4, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":3, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":3, + "aktivitas":0, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":2, + "hari":3, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":0, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":3, + "aktivitas":3, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":3, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":1, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":2, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":2, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":1, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":1, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":0, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":12, + "label":"moderate" + }, + { + "emosi":1, + "aktivitas":0, + "hari":5, + "gadScore":10, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":4, + "hari":6, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":9, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":3, + "aktivitas":0, + "hari":1, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":3, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":4, + "aktivitas":0, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":5, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":3, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":4, + "gadScore":11, + "label":"moderate" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":6, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":1, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":4, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":2, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":2, + "hari":6, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":2, + "hari":5, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":2, + "hari":6, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":3, + "aktivitas":1, + "hari":5, + "gadScore":8, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":3, + "aktivitas":3, + "hari":2, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":3, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":5, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":0, + "hari":0, + "gadScore":5, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":1, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":0, + "hari":2, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":0, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":6, + "gadScore":7, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":3, + "hari":0, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":3, + "hari":1, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":0, + "aktivitas":3, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":2, + "hari":3, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":3, + "hari":4, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":5, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":3, + "hari":6, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":0, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":1, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":2, + "aktivitas":1, + "hari":2, + "gadScore":4, + "label":"minimal" + }, + { + "emosi":2, + "aktivitas":1, + "hari":3, + "gadScore":2, + "label":"minimal" + }, + { + "emosi":1, + "aktivitas":0, + "hari":4, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":0, + "aktivitas":3, + "hari":5, + "gadScore":6, + "label":"ringan" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":18, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":3, + "gadScore":17, + "label":"parah" + }, + { + "emosi":2, + "aktivitas":0, + "hari":4, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":17, + "label":"parah" + }, + { + "emosi":0, + "aktivitas":1, + "hari":6, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":17, + "label":"parah" + }, + { + "emosi":4, + "aktivitas":0, + "hari":3, + "gadScore":17, + "label":"parah" + }, + { + "emosi":4, + "aktivitas":0, + "hari":4, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":17, + "label":"parah" + }, + { + "emosi":4, + "aktivitas":0, + "hari":1, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":2, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":3, + "gadScore":16, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":4, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":5, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":0, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":0, + "hari":1, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":2, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":3, + "gadScore":17, + "label":"parah" + }, + { + "emosi":4, + "aktivitas":1, + "hari":4, + "gadScore":17, + "label":"parah" + }, + { + "emosi":4, + "aktivitas":1, + "hari":5, + "gadScore":17, + "label":"parah" + }, + { + "emosi":1, + "aktivitas":1, + "hari":6, + "gadScore":17, + "label":"parah" + } +] \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/MainActivity.kt b/app/src/main/java/com/example/app_skripsi/MainActivity.kt new file mode 100644 index 0000000..e3a3a10 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/MainActivity.kt @@ -0,0 +1,212 @@ +package com.example.app_skripsi + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.window.SplashScreen +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.AppDatabase +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.data.repository.UserRepository +import com.example.app_skripsi.databinding.ActivityMainBinding +import com.example.app_skripsi.ui.auth.login.LoginActivity +import com.example.app_skripsi.ui.dashboard.DashboardActivity +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +import android.Manifest +import android.content.pm.PackageManager +import android.util.Log + +import androidx.activity.result.contract.ActivityResultContracts +import androidx.startup.AppInitializer +import app.rive.runtime.kotlin.RiveInitializer +import app.rive.runtime.kotlin.core.Rive +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.utils.NotificationSchedulerManager + +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class MainActivity : AppCompatActivity() { + private var _binding : ActivityMainBinding? = null + private val binding get() = _binding!! + private lateinit var sessionManager: SessionManager + // ✅ Initialize userRepository properly + private val userRepository by lazy { + val database = AppDatabase.getDatabase(this) + UserRepository(FirebaseService(), database.userDao()) + } + + private lateinit var routineSessionManager: RoutineSessionManager + private lateinit var notificationManager: NotificationSchedulerManager + + // Permission request launcher harus dideklarasikan sebagai variable + private val permissionRequestLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + // Izin diberikan, lanjutkan aplikasi + Log.d("MainActivity", "Izin notifikasi diberikan") + startAppLogic() + } else { + // Izin ditolak, beri tahu pengguna tetapi tetap lanjutkan aplikasi + Log.d("MainActivity", "Izin notifikasi ditolak") + startAppLogic() // Tetap lanjutkan aplikasi meskipun izin ditolak + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + enableEdgeToEdge() + AppInitializer.getInstance(applicationContext) + .initializeComponent(RiveInitializer::class.java) + Rive.init(this) + _binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + sessionManager = SessionManager(this) + // Initialize session manager and notification scheduler + routineSessionManager = RoutineSessionManager(this) + notificationManager = NotificationSchedulerManager(this) + + // Periksa izin notifikasi hanya jika SDK >= 33 (Android 13) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + checkNotificationPermission() + } else{ + startAppLogic() + } + + // Check if we need to start routine form reminders + checkAndScheduleRoutineReminders() + // Handle deep links from notifications if needed + handleNotificationIntent(intent.extras) + + } + + // Fungsi untuk memeriksa izin + private fun checkNotificationPermission() { + if (ContextCompat.checkSelfPermission( + this, Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED) { + // Izin sudah diberikan + Log.d("MainActivity", "Izin notifikasi sudah diberikan") + startAppLogic() + } else { + // Izin belum diberikan, minta izin + Log.d("MainActivity", "Izin notifikasi belum diberikan, meminta izin") + requestNotificationPermission() + } + } + + // Fungsi untuk meminta izin notifikasi + private fun requestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionRequestLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + startAppLogic() + } + } + + /** + * Checks if routine session is active and schedules form reminders if needed + */ + private fun checkAndScheduleRoutineReminders() { + lifecycleScope.launch { + try { + // Dapatkan user ID saat ini + val userId = sessionManager.sessionUserId.first() + if (userId.isNullOrEmpty()) { + Log.d("MainActivity", "No user logged in, skipping routine reminders") + return@launch + } + // Restore session dari Firebase jika ada + val isSessionRestored = routineSessionManager.restoreSessionFromFirebase(FirebaseService()) + + if (isSessionRestored || routineSessionManager.isSessionStillActive()) { + Log.d("MainActivity", "Active routine session found for user: $userId, scheduling reminders") + + // Ubah ke AlarmManager untuk routine reminders + notificationManager.scheduleRoutineFormRemindersWithAlarm(userId) + } else { + Log.d("MainActivity", "No active routine session, cancelling reminders") + notificationManager.cancelRoutineFormAlarms() + } + } catch (e: Exception) { + Log.e("MainActivity", "Error checking routine session", e) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + + // Handle intent dari notifikasi + handleNotificationIntent(intent.extras) + } + + private fun handleNotificationIntent(extras: Bundle?) { + if (extras == null) return + + val openForm = extras.getBoolean("OPEN_FORM", false) + val detectionType = extras.getString("DETECTION_TYPE", null) + + if (openForm && detectionType == "ROUTINE") { + Log.d("MainActivity", "Opening routine form from notification") + + // Buat intent untuk membuka DashboardActivity dengan flag + val dashboardIntent = Intent(this, DashboardActivity::class.java).apply { + putExtra("NAVIGATE_TO", "CHECK_ANXIETY") + putExtra("DETECTION_TYPE", "ROUTINE") + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + startActivity(dashboardIntent) + } + } + + + private fun startAppLogic() { + lifecycleScope.launch { + delay(500) + binding.tvNamaApp.alpha = 0f + binding.tvNamaApp.animate().setDuration(3000).alpha(1f).withEndAction { + lifecycleScope.launch { + delay(500) + + sessionManager.sessionUserId.collect { userId -> + if (!userId.isNullOrEmpty()) { + Log.d("MainActivity", "📦 User Loaded from Session: $userId") + // Setelah login, check dan schedule reminders + checkAndScheduleRoutineReminders() + // Handle intent dari notifikasi jika ada + handleNotificationIntent(intent.extras) + startActivity(Intent(this@MainActivity, DashboardActivity::class.java)) + } else { + Log.e("MainActivity", "❌ User ID Not Found, Redirecting to Login") + startActivity(Intent(this@MainActivity, LoginActivity::class.java)) + } + finish() + } + } + + + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/core/knn/DataPoint.kt b/app/src/main/java/com/example/app_skripsi/core/knn/DataPoint.kt new file mode 100644 index 0000000..b6fa617 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/knn/DataPoint.kt @@ -0,0 +1,10 @@ +// 1. DataPoint.kt +package com.example.app_skripsi.core.knn + +data class DataPoint( + val emosi: Int, + val aktivitas: Int, + val hari: Int, + val gadScore: Int, + val label: String +) diff --git a/app/src/main/java/com/example/app_skripsi/core/knn/HybridPredictionService.kt b/app/src/main/java/com/example/app_skripsi/core/knn/HybridPredictionService.kt new file mode 100644 index 0000000..7832ecf --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/knn/HybridPredictionService.kt @@ -0,0 +1,351 @@ +package com.example.app_skripsi.core.knn + +import android.content.Context +import android.util.Log +import com.example.app_skripsi.data.model.DailyDetectionData + +class HybridPredictionService(private val context: Context) { + + companion object { + private const val TAG = "HybridPredictionService" + + // Prediction modes + const val MODE_GLOBAL = 0 // Use only global training data + const val MODE_PERSONAL = 1 // Use only personal data + const val MODE_HYBRID = 2 // Use both global and personal data (default) + } + + // Mapping for numeric to string conversions + private val emosiStringMapping = mapOf( + 0 to "Senang", 1 to "Sedih", 2 to "Normal", 3 to "Marah", 4 to "Kecewa" + ) + + private val aktivitasStringMapping = mapOf( + 0 to "Belajar/Bekerja", 1 to "Istirahat", 2 to "Hiburan", + 3 to "Sosialisasi", 4 to "Olahraga" + ) + + private val hariStringMapping = mapOf( + 0 to "Senin", 1 to "Selasa", 2 to "Rabu", + 3 to "Kamis", 4 to "Jumat", 5 to "Sabtu", 6 to "Minggu" + ) + + // Reverse mapping (string to numeric) + private val emosiMapping = mapOf( + "Senang" to 0, "Sedih" to 1, "Normal" to 2, "Marah" to 3, "Kecewa" to 4 + ) + + private val aktivitasMapping = mapOf( + "Belajar/Bekerja" to 0, "Istirahat" to 1, "Hiburan" to 2, + "Sosialisasi" to 3, "Olahraga" to 4 + ) + + /** + * Creates a KNN classifier based on the selected mode + */ + private fun createClassifier( + detectionHistory: List, + mode: Int = MODE_HYBRID + ): KNNClassifier { + try { + when (mode) { + MODE_GLOBAL -> { + // Use only global training data + val globalData = KNNUtils.loadTrainingData(context) + Log.d(TAG, "Created global classifier with ${globalData.size} training points") + return KNNClassifier(globalData) + } + + MODE_PERSONAL -> { + // Use only personal data + if (detectionHistory.isEmpty()) { + Log.w(TAG, "No personal data available, falling back to minimal dataset") + return KNNClassifier(createMinimalTrainingData()) + } + + val personalData = convertUserHistoryToDataPoints(detectionHistory) + Log.d(TAG, "Created personal classifier with ${personalData.size} training points") + return KNNClassifier(personalData, k = minOf(3, personalData.size)) + } + + MODE_HYBRID -> { + // Combine global and personal data with personal data weighted more heavily + val globalData = KNNUtils.loadTrainingData(context) + + if (detectionHistory.isEmpty()) { + Log.d(TAG, "No personal data available for hybrid mode, using only global data") + return KNNClassifier(globalData) + } + + val personalData = convertUserHistoryToDataPoints(detectionHistory) + + // Duplicate personal data to give it higher weight (3x) + val combinedData = globalData + personalData + personalData + personalData + Log.d(TAG, "Created hybrid classifier with ${combinedData.size} training points " + + "(${globalData.size} global + ${personalData.size} personal)") + + return KNNClassifier(combinedData) + } + + else -> { + Log.w(TAG, "Invalid mode $mode, using hybrid mode") + return createClassifier(detectionHistory, MODE_HYBRID) + } + } + } catch (e: Exception) { + Log.e(TAG, "Error creating classifier: ${e.message}", e) + return KNNClassifier(createMinimalTrainingData()) + } + } + + /** + * Converts user history data to KNN DataPoint format + */ + private fun convertUserHistoryToDataPoints(history: List): List { + return history.map { data -> + DataPoint( + emosi = emosiMapping[data.emosi] ?: 2, + aktivitas = aktivitasMapping[data.kegiatan] ?: 1, + hari = getDayOfWeek(data.tanggal.toDate()), + gadScore = data.totalSkor, + label = data.severity.lowercase() + ) + } + } + + /** + * Creates minimal training data as fallback + */ + private fun createMinimalTrainingData(): List { + return listOf( + DataPoint(emosi = 0, aktivitas = 0, hari = 0, gadScore = 3, label = "minimal"), + DataPoint(emosi = 0, aktivitas = 0, hari = 1, gadScore = 7, label = "ringan"), + DataPoint(emosi = 2, aktivitas = 0, hari = 2, gadScore = 12, label = "moderate"), + DataPoint(emosi = 4, aktivitas = 0, hari = 3, gadScore = 17, label = "parah") + ) + } + + /** + * Gets day of week (0-6) from Date + */ + private fun getDayOfWeek(date: java.util.Date): Int { + val calendar = java.util.Calendar.getInstance() + calendar.time = date + return (calendar.get(java.util.Calendar.DAY_OF_WEEK) - 1) % 7 // 0-6, Sunday-Saturday + } + + + fun predictNextSevenDaysDetail( + lastDayNumber: Int, + detectionHistory: List, + predictionMode: Int = MODE_HYBRID + ): List { + try { + // Log number of historical data points + Log.d(TAG, "Predicting with ${detectionHistory.size} historical data points, mode=$predictionMode") + + if (detectionHistory.isEmpty()) { + Log.w(TAG, "No historical data available for prediction") + return emptyList() + } + + // Calculate trend from history + val gadScoreTrend = calculateGadScoreTrend(detectionHistory) + Log.d(TAG, "GAD score trend: $gadScoreTrend") + + // Find emotion and activity patterns + val emosiFrequency = detectionHistory + .groupingBy { it.emosi } + .eachCount() + .maxByOrNull { it.value } + ?.key ?: "Normal" + + val aktivitasFrequency = detectionHistory + .groupingBy { it.kegiatan } + .eachCount() + .maxByOrNull { it.value } + ?.key ?: "Istirahat" + + // Map to numeric values (using defaults if not found) + val mostCommonEmosi = emosiMapping[emosiFrequency] ?: 2 + val mostCommonAktivitas = aktivitasMapping[aktivitasFrequency] ?: 1 + + // Use last emotion and activity too + val lastEmosi = emosiMapping[detectionHistory.lastOrNull()?.emosi] ?: 2 + val lastAktivitas = aktivitasMapping[detectionHistory.lastOrNull()?.kegiatan] ?: 1 + + Log.d(TAG, "Most common emotion: $emosiFrequency ($mostCommonEmosi)") + Log.d(TAG, "Most common activity: $aktivitasFrequency ($mostCommonAktivitas)") + Log.d(TAG, "Last emotion: ${detectionHistory.lastOrNull()?.emosi} ($lastEmosi)") + Log.d(TAG, "Last activity: ${detectionHistory.lastOrNull()?.kegiatan} ($lastAktivitas)") + + // Create classifier based on the selected mode + val knnClassifier = createClassifier(detectionHistory, predictionMode) + + // Predict next 7 days with details + val predictions = mutableListOf() + + for (i in 1..7) { + val nextDay = lastDayNumber + i + val dayOfWeek = nextDay % 7 // 0-6, Sunday-Saturday + val predictedGadScore = calculatePredictedGadScore(detectionHistory, gadScoreTrend, i) + + Log.d(TAG, "Predicting for day $nextDay (dayOfWeek=$dayOfWeek): predicted GAD score = $predictedGadScore") + + // Use both last and most frequent emotion/activity for prediction + val detailsLastEmosi = knnClassifier.getPredictionDetails( + lastEmosi, lastAktivitas, dayOfWeek, predictedGadScore + ) + + val detailsFrequentEmosi = knnClassifier.getPredictionDetails( + mostCommonEmosi, mostCommonAktivitas, dayOfWeek, predictedGadScore + ) + + // Choose prediction with higher confidence + val confidenceLast = detailsLastEmosi["confidence"] as Double + val confidenceFrequent = detailsFrequentEmosi["confidence"] as Double + + Log.d(TAG, "Last emotion confidence: $confidenceLast, Most common confidence: $confidenceFrequent") + + val finalDetails = if (confidenceLast >= confidenceFrequent) detailsLastEmosi else detailsFrequentEmosi + + // Extract important factors + val importantFactors = finalDetails["importantFactors"] as Map + val dominantEmosi = importantFactors["emosi"] as Int + val dominantAktivitas = importantFactors["aktivitas"] as Int + + val finalPrediction = finalDetails["prediction"] as String + Log.d(TAG, "Final prediction for day $nextDay: $finalPrediction (confidence: ${finalDetails["confidence"]})") + + // Create detailed prediction + predictions.add( + PredictionDetail( + day = nextDay, + dayOfWeek = hariStringMapping[dayOfWeek] ?: "Unknown", + predictedSeverity = finalPrediction, + confidence = finalDetails["confidence"] as Double, + predictedGadScore = predictedGadScore, + suggestedEmosi = emosiStringMapping[dominantEmosi] ?: "Normal", + suggestedAktivitas = aktivitasStringMapping[dominantAktivitas] ?: "Istirahat", + predictionMode = getPredictionModeName(predictionMode) + ) + ) + } + + return predictions + } catch (e: Exception) { + Log.e(TAG, "Error in prediction: ${e.message}", e) + return emptyList() + } + } + + /** + * Gets readable name for prediction mode + */ + private fun getPredictionModeName(mode: Int): String { + return when (mode) { + MODE_GLOBAL -> "Data Global" + MODE_PERSONAL -> "Data Personal" + MODE_HYBRID -> "Data Hybrid" + else -> "Data Hybrid" + } + } + + /** + * Data class for detailed prediction results + */ + data class PredictionDetail( + val day: Int, + val dayOfWeek: String, + val predictedSeverity: String, + val confidence: Double, + val predictedGadScore: Int, + val suggestedEmosi: String, + val suggestedAktivitas: String, + val predictionMode: String + ) { + /** + * Gets readable description of prediction + */ + fun getReadableDescription(): String { + val severityLabel = when (predictedSeverity) { + "Minimal" -> "tingkat kecemasan minimal" + "Ringan" -> "tingkat kecemasan ringan" + "Sedang" -> "tingkat kecemasan sedang" + "Parah" -> "tingkat kecemasan parah" + else -> "tingkat kecemasan yang tidak dapat diprediksi" + } + + return "Pada hari $dayOfWeek, Anda cenderung mengalami $severityLabel " + + "ketika melakukan aktivitas $suggestedAktivitas dengan emosi $suggestedEmosi." + } + + /** + * Gets recommended activity based on predicted severity + */ + fun getRecommendedActivity(): String { + return when (predictedSeverity) { + "Minimal" -> "Pertahankan aktivitas seperti ${suggestedAktivitas.lowercase()} dan coba aktivitas baru yang menyenangkan." + "Ringan" -> "Pertimbangkan melakukan aktivitas relaksasi seperti yoga atau meditasi di samping ${suggestedAktivitas.lowercase()}." + "Sedang" -> "Disarankan untuk menyisihkan waktu istirahat yang cukup dan berbicara dengan teman dekat atau keluarga." + "Parah" -> "Sangat disarankan untuk berkonsultasi dengan profesional kesehatan mental dan kurangi beban aktivitas." + else -> "Jaga keseimbangan antara aktivitas dan waktu istirahat Anda." + } + } + + /** + * Gets complete description with recommendations + */ + fun getCompleteDescription(): String { + return "${getReadableDescription()} ${getRecommendedActivity()}" + } + } + + /** + * For backward compatibility + */ + fun predictNextSevenDays( + lastDayNumber: Int, + detectionHistory: List, + predictionMode: Int = MODE_HYBRID + ): List> { + return predictNextSevenDaysDetail(lastDayNumber, detectionHistory, predictionMode) + .map { Pair(it.day, it.predictedSeverity) } + } + + /** + * Calculates trend in GAD scores from history data + */ + private fun calculateGadScoreTrend(history: List): Double { + if (history.size < 2) return 0.0 + + // Simple linear regression for GAD score trend + val gadScores = history.map { it.totalSkor } + val x = (0 until gadScores.size).toList() + + val sumX = x.sum().toDouble() + val sumY = gadScores.sum().toDouble() + val sumXY = x.zip(gadScores).sumOf { (x, y) -> x * y }.toDouble() + val sumXX = x.sumOf { it * it }.toDouble() + val n = x.size + + return (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX) + } + + /** + * Calculates predicted GAD score based on history and trend + */ + private fun calculatePredictedGadScore( + history: List, + trend: Double, + daysAhead: Int + ): Int { + if (history.isEmpty()) return 5 // Default value + + val lastGadScore = history.last().totalSkor + val predictedScore = lastGadScore + (trend * daysAhead) + + // Keep score within GAD-7 range (0-21) + return predictedScore.coerceIn(0.0, 21.0).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/core/knn/KNNClasifier.kt b/app/src/main/java/com/example/app_skripsi/core/knn/KNNClasifier.kt new file mode 100644 index 0000000..5f15efa --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/knn/KNNClasifier.kt @@ -0,0 +1,275 @@ +package com.example.app_skripsi.core.knn + +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.math.max +import kotlin.math.min +import android.util.Log + +/** + * Enhanced K-Nearest Neighbors classifier for anxiety severity prediction. + * Features include: + * - Feature normalization + * - Weighted distance calculations + * - Confidence scoring + * - Threshold-based severity classification + * - Important factor identification + */ +class KNNClassifier( + private val trainingData: List, + private val k: Int = 5, + private val weights: List = listOf(1.5, 1.0, 0.5, 3.0) // Emotion, Activity, Day, GAD Score +) { + companion object { + private const val TAG = "KNNClassifier" + + // GAD-7 severity thresholds + const val MINIMAL_MAX = 4 + const val MILD_MAX = 9 + const val MODERATE_MAX = 14 + // Anything above MODERATE_MAX is considered "parah" + } + + // Store min and max values for normalization + private val featureRanges: List> = calculateFeatureRanges() + + private fun calculateFeatureRanges(): List> { + // For each feature, find min and max values + val features = listOf( + trainingData.map { it.emosi }, + trainingData.map { it.aktivitas }, + trainingData.map { it.hari }, + trainingData.map { it.gadScore } + ) + + return features.map { values -> + Pair(values.minOrNull()?.toDouble() ?: 0.0, values.maxOrNull()?.toDouble() ?: 1.0) + } + } + + /** + * Predicts anxiety severity using the KNN algorithm with manual score override + */ + fun predict(emosi: Int, aktivitas: Int, hari: Int, gadScore: Int): String { + // First predict using KNN algorithm + val knnPrediction = predictBasedOnNeighbors(emosi, aktivitas, hari, gadScore) + + // Override prediction based on actual GAD score if it's significantly different + val scoreBasedSeverity = getSeverityFromScore(gadScore) + + // Log both predictions for debugging + Log.d(TAG, "KNN Prediction: $knnPrediction, Score-based severity: $scoreBasedSeverity for score $gadScore") + + // If GAD score is in "Sedang" or "Parah" range, prioritize it over KNN prediction + // This ensures high anxiety scores aren't predicted as lower severities + if ((scoreBasedSeverity == "Sedang" || scoreBasedSeverity == "Parah") && + (knnPrediction == "Minimal" || knnPrediction == "Ringan")) { + Log.d(TAG, "Overriding KNN prediction with score-based severity: $scoreBasedSeverity") + return scoreBasedSeverity + } + + // Similarly, if GAD score is very low, make sure we don't predict high anxiety + if (scoreBasedSeverity == "Minimal" && + (knnPrediction == "Sedang" || knnPrediction == "Parah")) { + Log.d(TAG, "Overriding KNN prediction with score-based severity: $scoreBasedSeverity") + return scoreBasedSeverity + } + + return knnPrediction + } + + /** + * Get severity level based on GAD-7 score + */ + private fun getSeverityFromScore(gadScore: Int): String { + return when { + gadScore <= MINIMAL_MAX -> "Minimal" + gadScore <= MILD_MAX -> "Ringan" + gadScore <= MODERATE_MAX -> "Sedang" + else -> "Parah" + } + } + + /** + * Make prediction based on k-nearest neighbors + */ + private fun predictBasedOnNeighbors(emosi: Int, aktivitas: Int, hari: Int, gadScore: Int): String { + val input = listOf(emosi, aktivitas, hari, gadScore) + val distances = trainingData.map { + val dist = euclideanDistance( + normalizeFeatures(input), + normalizeFeatures(listOf(it.emosi, it.aktivitas, it.hari, it.gadScore)) + ) + Pair(dist, it.label) + }.sortedBy { it.first } + + val kNearest = distances.take(k).map { it.second } + val prediction = kNearest.groupingBy { it }.eachCount().maxByOrNull { it.value }?.key ?: "ringan" + + // Normalize label before returning + return normalizeLabel(prediction) + } + + /** + * Normalizes features to a 0-1 range for better comparison + */ + private fun normalizeFeatures(features: List): List { + return features.mapIndexed { index, value -> + val (min, max) = featureRanges[index] + if (max == min) 0.0 else (value - min) / (max - min) + } + } + + /** + * Calculates weighted Euclidean distance between two feature vectors + */ + private fun euclideanDistance(a: List, b: List): Double { + return sqrt(a.zip(b).zip(weights).sumOf { (pair, weight) -> + val (ai, bi) = pair + weight * (ai - bi).pow(2) + }) + } + + /** + * Evaluates the model using test data + */ + fun evaluateModel(testData: List): Double { + var correctPredictions = 0 + + testData.forEach { point -> + val prediction = predict(point.emosi, point.aktivitas, point.hari, point.gadScore) + if (normalizeLabel(point.label) == prediction) { + correctPredictions++ + } + } + + return correctPredictions.toDouble() / testData.size + } + + /** + * Predicts anxiety severity with confidence score + */ + fun predictWithConfidence(emosi: Int, aktivitas: Int, hari: Int, gadScore: Int): Pair { + val input = listOf(emosi, aktivitas, hari, gadScore) + val distances = trainingData.map { + val dist = euclideanDistance( + normalizeFeatures(input), + normalizeFeatures(listOf(it.emosi, it.aktivitas, it.hari, it.gadScore)) + ) + Pair(dist, it.label) + }.sortedBy { it.first } + + val kNearest = distances.take(k) + val labelCounts = kNearest.groupingBy { it.second }.eachCount() + val rawPrediction = labelCounts.maxByOrNull { it.value }?.key ?: "ringan" + + // Get prediction based on KNN with scoring override + val finalPrediction = predict(emosi, aktivitas, hari, gadScore) + + // Calculate confidence + val knnConfidence = labelCounts[rawPrediction]?.toDouble()?.div(k) ?: 0.0 + + // Adjust confidence based on how close GAD score is to threshold boundaries + val scoreBasedSeverity = getSeverityFromScore(gadScore) + val adjustedConfidence = if (finalPrediction == scoreBasedSeverity) { + // Boost confidence if KNN and score prediction match + min(1.0, knnConfidence * 1.2) + } else { + // Reduce confidence if they don't match + max(0.3, knnConfidence * 0.8) + } + + return Pair(finalPrediction, adjustedConfidence) + } + + /** + * Gets detailed prediction information including important factors + */ + fun getPredictionDetails(emosi: Int, aktivitas: Int, hari: Int, gadScore: Int): Map { + val input = listOf(emosi, aktivitas, hari, gadScore) + val distances = trainingData.map { + val dist = euclideanDistance( + normalizeFeatures(input), + normalizeFeatures(listOf(it.emosi, it.aktivitas, it.hari, it.gadScore)) + ) + Pair(dist, it) + }.sortedBy { it.first } + + val kNearest = distances.take(k) + val labelCounts = kNearest.groupingBy { it.second.label }.eachCount() + val rawPrediction = labelCounts.maxByOrNull { it.value }?.key ?: "ringan" + + // Use improved prediction that considers GAD score directly + val finalPrediction = predict(emosi, aktivitas, hari, gadScore) + + // Log details of k-nearest neighbors for debugging + Log.d(TAG, "K-Nearest Neighbors for input (e=$emosi, a=$aktivitas, h=$hari, g=$gadScore):") + kNearest.forEachIndexed { index, (distance, dataPoint) -> + Log.d(TAG, "$index: Distance=$distance, Label=${dataPoint.label}, " + + "e=${dataPoint.emosi}, a=${dataPoint.aktivitas}, h=${dataPoint.hari}, g=${dataPoint.gadScore}") + } + + // Log label counts for debugging + Log.d(TAG, "Label counts: $labelCounts") + Log.d(TAG, "KNN raw prediction: ${normalizeLabel(rawPrediction)}, Final prediction: $finalPrediction") + + // Most influential factors + val importantFactors = determineImportantFactors(kNearest.map { it.second }) + + // Calculate confidence based on label distribution + val confidence = if (finalPrediction == normalizeLabel(rawPrediction)) { + labelCounts[rawPrediction]?.toDouble()?.div(k) ?: 0.0 + } else { + // If we overrode the KNN prediction, adjust confidence + val scoreBasedConfidence = 0.7 // Base confidence for score-based prediction + // Further adjust if we're close to a threshold + when (gadScore) { + 4, 5, 9, 10, 14, 15 -> 0.6 // Near thresholds, slightly less confident + else -> scoreBasedConfidence + } + } + + return mapOf( + "prediction" to finalPrediction, + "confidence" to confidence, + "importantFactors" to importantFactors, + "originalKnnPrediction" to normalizeLabel(rawPrediction), + "scoreBasedSeverity" to getSeverityFromScore(gadScore), + "gadScore" to gadScore + ) + } + + /** + * Determines the most important factors in the prediction + */ + private fun determineImportantFactors(neighbors: List): Map { + // Calculate average and mode for each feature + val emosiCounts = neighbors.groupingBy { it.emosi }.eachCount() + val aktivityCounts = neighbors.groupingBy { it.aktivitas }.eachCount() + val hariCounts = neighbors.groupingBy { it.hari }.eachCount() + + val dominantEmosi = emosiCounts.maxByOrNull { it.value }?.key ?: 0 + val dominantAktivitas = aktivityCounts.maxByOrNull { it.value }?.key ?: 0 + val dominantHari = hariCounts.maxByOrNull { it.value }?.key ?: 0 + + return mapOf( + "emosi" to dominantEmosi, + "aktivitas" to dominantAktivitas, + "hari" to dominantHari + ) + } + + /** + * Normalizes label text to standardized format + */ + private fun normalizeLabel(label: String): String { + return when (label.lowercase()) { + "minimal" -> "Minimal" + "ringan" -> "Ringan" + "moderate" -> "Sedang" + "sedang" -> "Sedang" + "parah" -> "Parah" + else -> "Ringan" // Default if unknown + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/core/knn/KNNUtils.kt b/app/src/main/java/com/example/app_skripsi/core/knn/KNNUtils.kt new file mode 100644 index 0000000..8e9448e --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/knn/KNNUtils.kt @@ -0,0 +1,97 @@ +package com.example.app_skripsi.core.knn + +import android.content.Context +import android.util.Log +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.io.IOException + +/** + * Utility class for KNN operations, including loading training data + * and analyzing classification results. + */ +object KNNUtils { + private const val TAG = "KNNUtils" + private const val TRAINING_DATA_FILENAME = "TrainingData.json" + + /** + * Loads training data from a JSON file in assets + */ + fun loadTrainingData(context: Context): List { + try { + val inputStream = context.assets.open(TRAINING_DATA_FILENAME) + val json = inputStream.bufferedReader().use { it.readText() } + val type = object : TypeToken>() {}.type + + val data = Gson().fromJson>(json, type) + Log.d(TAG, "Successfully loaded ${data.size} training data points") + logTrainingDataSummary(data) + return data + } catch (e: IOException) { + Log.e(TAG, "Error loading training data: ${e.message}", e) + return emptyList() + } + } + + /** + * Logs summary of loaded training data for debugging + */ + private fun logTrainingDataSummary(data: List) { + if (data.isEmpty()) { + Log.w(TAG, "Training data is empty!") + return + } + + // Count occurrences of each severity level + val severityCounts = data.groupingBy { it.label.lowercase() }.eachCount() + + // Get statistics on GAD scores + val gadScores = data.map { it.gadScore } + val minScore = gadScores.minOrNull() ?: 0 + val maxScore = gadScores.maxOrNull() ?: 0 + val avgScore = gadScores.average() + + // Log summary + Log.d(TAG, "Training data summary:") + Log.d(TAG, "- Total data points: ${data.size}") + Log.d(TAG, "- Severity distribution: $severityCounts") + Log.d(TAG, "- GAD scores: min=$minScore, max=$maxScore, avg=$avgScore") + + // Verify proper mapping of severity labels to GAD score ranges + val incorrectMappings = data.filter { + !isGadScoreConsistentWithLabel(it.gadScore, it.label) + } + + if (incorrectMappings.isNotEmpty()) { + Log.w(TAG, "Found ${incorrectMappings.size} data points with inconsistent severity labels:") + incorrectMappings.take(5).forEach { + Log.w(TAG, " - GAD score: ${it.gadScore}, Label: ${it.label}") + } + } + } + + /** + * Checks if a GAD score is consistent with the given severity label + */ + private fun isGadScoreConsistentWithLabel(gadScore: Int, label: String): Boolean { + return when (label.lowercase()) { + "minimal" -> gadScore <= 4 + "ringan" -> gadScore in 5..9 + "moderate", "sedang" -> gadScore in 10..14 + "parah" -> gadScore >= 15 + else -> false + } + } + + /** + * Converts a GAD-7 score to the appropriate severity label + */ + fun getSeverityFromScore(score: Int): String { + return when { + score <= 4 -> "Minimal" + score <= 9 -> "Ringan" + score <= 14 -> "Sedang" + else -> "Parah" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/core/knn/PersonalPredictionService.kt b/app/src/main/java/com/example/app_skripsi/core/knn/PersonalPredictionService.kt new file mode 100644 index 0000000..bc00ae6 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/knn/PersonalPredictionService.kt @@ -0,0 +1,313 @@ +package com.example.app_skripsi.core.knn + +import android.content.Context +import android.util.Log +import com.example.app_skripsi.data.model.DailyDetectionData + +/** + * Service for predicting anxiety levels using personal data from the user's history. + * This implementation focuses on using only the user's own data to make predictions + * that are more relevant to their personal patterns. + */ +class PersonalPredictionService(private val context: Context) { + + companion object { + private const val TAG = "PersonalPredictionService" + } + + // Mapping for numeric to string conversions + private val emosiStringMapping = mapOf( + 0 to "Senang", 1 to "Sedih", 2 to "Normal", 3 to "Marah", 4 to "Kecewa" + ) + + private val aktivitasStringMapping = mapOf( + 0 to "Belajar/Bekerja", 1 to "Istirahat", 2 to "Hiburan", + 3 to "Sosialisasi", 4 to "Olahraga" + ) + + private val hariStringMapping = mapOf( + 0 to "Senin", 1 to "Selasa", 2 to "Rabu", + 3 to "Kamis", 4 to "Jumat", 5 to "Sabtu", 6 to "Minggu" + ) + + // Reverse mapping (string to numeric) + private val emosiMapping = mapOf( + "Senang" to 0, "Sedih" to 1, "Normal" to 2, "Marah" to 3, "Kecewa" to 4 + ) + + private val aktivitasMapping = mapOf( + "Belajar/Bekerja" to 0, "Istirahat" to 1, "Hiburan" to 2, + "Sosialisasi" to 3, "Olahraga" to 4 + ) + + /** + * Creates a KNN classifier based on personal data + */ + private fun createPersonalClassifier(detectionHistory: List): KNNClassifier { + try { + if (detectionHistory.isEmpty()) { + Log.w(TAG, "No personal data available, falling back to minimal dataset") + return KNNClassifier(createMinimalTrainingData()) + } + + // Convert history to data points for training + val personalData = convertUserHistoryToDataPoints(detectionHistory) + Log.d(TAG, "Created personal classifier with ${personalData.size} training points") + + // Use smaller k for small datasets to prevent overgeneralization + val adaptiveK = when { + personalData.size <= 3 -> 1 + personalData.size <= 7 -> 3 + else -> 5 + } + + return KNNClassifier(personalData, k = adaptiveK) + } catch (e: Exception) { + Log.e(TAG, "Error creating classifier: ${e.message}", e) + return KNNClassifier(createMinimalTrainingData()) + } + } + + /** + * Converts user history data to KNN DataPoint format + */ + private fun convertUserHistoryToDataPoints(history: List): List { + return history.map { data -> + val dayOfWeek = getDayOfWeek(data.tanggal.toDate()) + val emosi = emosiMapping[data.emosi] ?: 2 + val aktivitas = aktivitasMapping[data.kegiatan] ?: 1 + + // Log conversion for debugging + Log.d(TAG, "Converting history: emosi=${data.emosi}→$emosi, " + + "aktivitas=${data.kegiatan}→$aktivitas, hari=$dayOfWeek, " + + "gadScore=${data.totalSkor}, severity=${data.severity}") + + DataPoint( + emosi = emosi, + aktivitas = aktivitas, + hari = dayOfWeek, + gadScore = data.totalSkor, + label = data.severity.lowercase() + ) + } + } + + /** + * Creates minimal training data as fallback + */ + private fun createMinimalTrainingData(): List { + return listOf( + DataPoint(emosi = 0, aktivitas = 0, hari = 0, gadScore = 3, label = "minimal"), + DataPoint(emosi = 0, aktivitas = 0, hari = 1, gadScore = 7, label = "ringan"), + DataPoint(emosi = 2, aktivitas = 0, hari = 2, gadScore = 12, label = "sedang"), + DataPoint(emosi = 4, aktivitas = 0, hari = 3, gadScore = 17, label = "parah") + ) + } + + /** + * Gets day of week (0-6) from Date + */ + private fun getDayOfWeek(date: java.util.Date): Int { + val calendar = java.util.Calendar.getInstance() + calendar.time = date + // Convert from Java Calendar's 1-7 (Sunday-Saturday) to 0-6 (Sunday-Saturday) + return (calendar.get(java.util.Calendar.DAY_OF_WEEK) - 1) % 7 + } + + /** + * Predicts anxiety levels for the next 7 days based on user history + */ + fun predictNextSevenDaysDetail( + lastDayNumber: Int, + detectionHistory: List + ): List { + try { + Log.d(TAG, "Predicting with ${detectionHistory.size} personal data points") + + if (detectionHistory.isEmpty()) { + Log.w(TAG, "No historical data available for prediction") + return emptyList() + } + + // Calculate trend from history (how anxiety scores are changing over time) + val gadScoreTrend = calculateGadScoreTrend(detectionHistory) + Log.d(TAG, "GAD score trend: $gadScoreTrend") + + // Find patterns in emotion and activity + val emosiFrequency = detectionHistory + .groupingBy { it.emosi } + .eachCount() + .maxByOrNull { it.value } + ?.key ?: "Normal" + + val aktivitasFrequency = detectionHistory + .groupingBy { it.kegiatan } + .eachCount() + .maxByOrNull { it.value } + ?.key ?: "Istirahat" + + // Map to numeric values (using defaults if not found) + val mostCommonEmosi = emosiMapping[emosiFrequency] ?: 2 + val mostCommonAktivitas = aktivitasMapping[aktivitasFrequency] ?: 1 + + // Get the most recent emotion and activity + val lastEmosi = emosiMapping[detectionHistory.lastOrNull()?.emosi] ?: 2 + val lastAktivitas = aktivitasMapping[detectionHistory.lastOrNull()?.kegiatan] ?: 1 + + Log.d(TAG, "Most common emotion: $emosiFrequency ($mostCommonEmosi)") + Log.d(TAG, "Most common activity: $aktivitasFrequency ($mostCommonAktivitas)") + Log.d(TAG, "Last emotion: ${detectionHistory.lastOrNull()?.emosi} ($lastEmosi)") + Log.d(TAG, "Last activity: ${detectionHistory.lastOrNull()?.kegiatan} ($lastAktivitas)") + + // Create classifier based on personal data + val knnClassifier = createPersonalClassifier(detectionHistory) + + // Predict next 7 days with details + val predictions = mutableListOf() + + for (i in 1..7) { + val nextDay = lastDayNumber + i + val dayOfWeek = nextDay % 7 // 0-6, Sunday-Saturday + + // Calculate predicted GAD score based on trend + val predictedGadScore = calculatePredictedGadScore(detectionHistory, gadScoreTrend, i) + + Log.d(TAG, "Predicting for day $nextDay (dayOfWeek=$dayOfWeek): predicted GAD score = $predictedGadScore") + + // Make two predictions - one based on last state, one based on most common state + val detailsLastEmosi = knnClassifier.getPredictionDetails( + lastEmosi, lastAktivitas, dayOfWeek, predictedGadScore + ) + + val detailsFrequentEmosi = knnClassifier.getPredictionDetails( + mostCommonEmosi, mostCommonAktivitas, dayOfWeek, predictedGadScore + ) + + // Choose prediction with higher confidence + val confidenceLast = detailsLastEmosi["confidence"] as Double + val confidenceFrequent = detailsFrequentEmosi["confidence"] as Double + + Log.d(TAG, "Last emotion confidence: $confidenceLast, Most common confidence: $confidenceFrequent") + + // Choose the prediction with higher confidence + val finalDetails = if (confidenceLast >= confidenceFrequent) detailsLastEmosi else detailsFrequentEmosi + + // Extract important factors + val importantFactors = finalDetails["importantFactors"] as Map + val dominantEmosi = importantFactors["emosi"] as Int + val dominantAktivitas = importantFactors["aktivitas"] as Int + + val finalPrediction = finalDetails["prediction"] as String + val finalConfidence = finalDetails["confidence"] as Double + + Log.d(TAG, "Final prediction for day $nextDay: $finalPrediction (confidence: $finalConfidence)") + + // Create detailed prediction + predictions.add( + PredictionDetail( + day = nextDay, + dayOfWeek = hariStringMapping[dayOfWeek] ?: "Unknown", + predictedSeverity = finalPrediction, + confidence = finalConfidence, + predictedGadScore = predictedGadScore, + suggestedEmosi = emosiStringMapping[dominantEmosi] ?: "Normal", + suggestedAktivitas = aktivitasStringMapping[dominantAktivitas] ?: "Istirahat" + ) + ) + } + + return predictions + } catch (e: Exception) { + Log.e(TAG, "Error in prediction: ${e.message}", e) + return emptyList() + } + } + + /** + * Data class for detailed prediction results + */ + data class PredictionDetail( + val day: Int, + val dayOfWeek: String, + val predictedSeverity: String, + val confidence: Double, + val predictedGadScore: Int, + val suggestedEmosi: String, + val suggestedAktivitas: String + ) { + /** + * Gets readable description of prediction + */ + fun getReadableDescription(): String { + val severityLabel = when (predictedSeverity) { + "Minimal" -> "tingkat kecemasan minimal" + "Ringan" -> "tingkat kecemasan ringan" + "Sedang" -> "tingkat kecemasan sedang" + "Parah" -> "tingkat kecemasan parah" + else -> "tingkat kecemasan yang tidak dapat diprediksi" + } + + return "Pada hari $dayOfWeek, Anda cenderung mengalami $severityLabel " + + "ketika melakukan aktivitas $suggestedAktivitas dengan emosi $suggestedEmosi." + } + + /** + * Gets recommended activity based on predicted severity + */ + fun getRecommendedActivity(): String { + return when (predictedSeverity) { + "Minimal" -> "Pertahankan aktivitas seperti ${suggestedAktivitas.lowercase()} dan coba aktivitas baru yang menyenangkan." + "Ringan" -> "Pertimbangkan melakukan aktivitas relaksasi seperti yoga atau meditasi di samping ${suggestedAktivitas.lowercase()}." + "Sedang" -> "Disarankan untuk menyisihkan waktu istirahat yang cukup dan berbicara dengan teman dekat atau keluarga." + "Parah" -> "Sangat disarankan untuk berkonsultasi dengan profesional kesehatan mental dan kurangi beban aktivitas." + else -> "Jaga keseimbangan antara aktivitas dan waktu istirahat Anda." + } + } + + /** + * Gets complete description with recommendations + */ + fun getCompleteDescription(): String { + return "${getReadableDescription()} ${getRecommendedActivity()}" + } + } + + /** + * Calculates trend in GAD scores from history data using linear regression + */ + private fun calculateGadScoreTrend(history: List): Double { + if (history.size < 2) return 0.0 + + // Simple linear regression for GAD score trend + val gadScores = history.map { it.totalSkor } + val x = (0 until gadScores.size).toList() + + val sumX = x.sum().toDouble() + val sumY = gadScores.sum().toDouble() + val sumXY = x.zip(gadScores).sumOf { (x, y) -> x * y }.toDouble() + val sumXX = x.sumOf { it * it }.toDouble() + val n = x.size + + // Prevent division by zero + if ((n * sumXX - sumX * sumX) == 0.0) return 0.0 + + return (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX) + } + + /** + * Calculates predicted GAD score based on history trend and GAD-7 constraints + */ + private fun calculatePredictedGadScore( + history: List, + trend: Double, + daysAhead: Int + ): Int { + if (history.isEmpty()) return 5 // Default value + + val lastGadScore = history.last().totalSkor + val predictedScore = lastGadScore + (trend * daysAhead) + + // Keep score within GAD-7 range (0-21) + return predictedScore.coerceIn(0.0, 21.0).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/core/knn/PredictionService.kt b/app/src/main/java/com/example/app_skripsi/core/knn/PredictionService.kt new file mode 100644 index 0000000..b0926a4 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/knn/PredictionService.kt @@ -0,0 +1,233 @@ +package com.example.app_skripsi.core.knn + +import android.content.Context +import android.util.Log +import com.example.app_skripsi.data.model.DailyDetectionData + +class PredictionService(private val context: Context) { + private val knnClassifier: KNNClassifier by lazy { + try { + val trainingData = KNNUtils.loadTrainingData(context) + Log.d("PredictionService", "Loaded ${trainingData.size} training data points") + KNNClassifier(trainingData) + } catch (e: Exception) { + Log.e("PredictionService", "Error loading training data: ${e.message}", e) + // Fallback to minimal dataset if there's an error + KNNClassifier(createMinimalTrainingData()) + } + } + + // Mapping untuk konversi nilai numerik ke string + private val emosiStringMapping = mapOf( + 0 to "Senang", 1 to "Sedih", 2 to "Normal", 3 to "Marah", 4 to "Kecewa" + ) + + private val aktivitasStringMapping = mapOf( + 0 to "Belajar/Bekerja", 1 to "Istirahat", 2 to "Hiburan", + 3 to "Sosialisasi", 4 to "Olahraga" + ) + + private val hariStringMapping = mapOf( + 0 to "Senin", 1 to "Selasa", 2 to "Rabu", + 3 to "Kamis", 4 to "Jumat", 5 to "Sabtu", 6 to "Minggu" + ) + + // Mapping string ke numerik (versi terbalik) + private val emosiMapping = mapOf( + "Senang" to 0, "Sedih" to 1, "Normal" to 2, "Marah" to 3, "Kecewa" to 4 + ) + + private val aktivitasMapping = mapOf( + "Belajar/Bekerja" to 0, "Istirahat" to 1, "Hiburan" to 2, + "Sosialisasi" to 3, "Olahraga" to 4 + ) + + // Minimal training data untuk fallback + private fun createMinimalTrainingData(): List { + return listOf( + DataPoint(emosi = 0, aktivitas = 0, hari = 0, gadScore = 3, label = "minimal"), + DataPoint(emosi = 0, aktivitas = 0, hari = 1, gadScore = 7, label = "ringan"), + DataPoint(emosi = 2, aktivitas = 0, hari = 2, gadScore = 12, label = "moderate"), + DataPoint(emosi = 4, aktivitas = 0, hari = 3, gadScore = 17, label = "parah") + ) + } + + fun predictNextSevenDaysDetail( + lastDayNumber: Int, + detectionHistory: List + ): List { + try { + // Log jumlah data historis + Log.d("PredictionService", "Predicting with ${detectionHistory.size} historical data points") + + if (detectionHistory.isEmpty()) { + Log.w("PredictionService", "No historical data available for prediction") + return emptyList() + } + + // Calculate trend from history + val gadScoreTrend = calculateGadScoreTrend(detectionHistory) + Log.d("PredictionService", "GAD score trend: $gadScoreTrend") + + // Mencari pola dan frekuensi emosi dan aktivitas + val emosiFrequency = detectionHistory + .groupingBy { it.emosi } + .eachCount() + .maxByOrNull { it.value } + ?.key ?: "Normal" + + val aktivitasFrequency = detectionHistory + .groupingBy { it.kegiatan } + .eachCount() + .maxByOrNull { it.value } + ?.key ?: "Istirahat" + + // Map ke nilai numerik (menggunakan default values jika tidak ditemukan) + val mostCommonEmosi = emosiMapping[emosiFrequency] ?: 2 + val mostCommonAktivitas = aktivitasMapping[aktivitasFrequency] ?: 1 + + // Menggunakan juga emosi dan aktivitas terakhir + val lastEmosi = emosiMapping[detectionHistory.lastOrNull()?.emosi] ?: 2 + val lastAktivitas = aktivitasMapping[detectionHistory.lastOrNull()?.kegiatan] ?: 1 + + Log.d("PredictionService", "Most common emotion: $emosiFrequency ($mostCommonEmosi)") + Log.d("PredictionService", "Most common activity: $aktivitasFrequency ($mostCommonAktivitas)") + Log.d("PredictionService", "Last emotion: ${detectionHistory.lastOrNull()?.emosi} ($lastEmosi)") + Log.d("PredictionService", "Last activity: ${detectionHistory.lastOrNull()?.kegiatan} ($lastAktivitas)") + + // Predict next 7 days with more details + val predictions = mutableListOf() + + for (i in 1..7) { + val nextDay = lastDayNumber + i + val dayOfWeek = nextDay % 7 // 0-6, Minggu-Sabtu + val predictedGadScore = calculatePredictedGadScore(detectionHistory, gadScoreTrend, i) + + Log.d("PredictionService", "Predicting for day $nextDay (dayOfWeek=$dayOfWeek): predicted GAD score = $predictedGadScore") + + // Gunakan kombinasi emosi/aktivitas yang berbeda untuk prediksi yang lebih handal + val detailsLastEmosi = knnClassifier.getPredictionDetails( + lastEmosi, lastAktivitas, dayOfWeek, predictedGadScore + ) + + val detailsFrequentEmosi = knnClassifier.getPredictionDetails( + mostCommonEmosi, mostCommonAktivitas, dayOfWeek, predictedGadScore + ) + + // Pilih prediksi dengan confidence lebih tinggi + val confidenceLast = detailsLastEmosi["confidence"] as Double + val confidenceFrequent = detailsFrequentEmosi["confidence"] as Double + + Log.d("PredictionService", "Last emotion confidence: $confidenceLast, Most common confidence: $confidenceFrequent") + + val finalDetails = if (confidenceLast >= confidenceFrequent) detailsLastEmosi else detailsFrequentEmosi + + // Ekstrak faktor penting + val importantFactors = finalDetails["importantFactors"] as Map + val dominantEmosi = importantFactors["emosi"] as Int + val dominantAktivitas = importantFactors["aktivitas"] as Int + + val finalPrediction = finalDetails["prediction"] as String + Log.d("PredictionService", "Final prediction for day $nextDay: $finalPrediction (confidence: ${finalDetails["confidence"]})") + + // Buat prediksi detail + predictions.add( + PredictionDetail( + day = nextDay, + dayOfWeek = hariStringMapping[dayOfWeek] ?: "Unknown", + predictedSeverity = finalPrediction, + confidence = finalDetails["confidence"] as Double, + predictedGadScore = predictedGadScore, + suggestedEmosi = emosiStringMapping[dominantEmosi] ?: "Normal", + suggestedAktivitas = aktivitasStringMapping[dominantAktivitas] ?: "Istirahat" + ) + ) + } + + return predictions + } catch (e: Exception) { + Log.e("PredictionService", "Error in prediction: ${e.message}", e) + return emptyList() + } + } + + // Data class untuk detail prediksi + data class PredictionDetail( + val day: Int, + val dayOfWeek: String, + val predictedSeverity: String, + val confidence: Double, + val predictedGadScore: Int, + val suggestedEmosi: String, + val suggestedAktivitas: String + ) { + // Fungsi untuk menghasilkan prediksi dalam format yang mudah dibaca + fun getReadableDescription(): String { + val severityLabel = when (predictedSeverity) { + "Minimal" -> "tingkat kecemasan minimal" + "Ringan" -> "tingkat kecemasan ringan" + "Sedang" -> "tingkat kecemasan sedang" + "Parah" -> "tingkat kecemasan parah" + else -> "tingkat kecemasan yang tidak dapat diprediksi" + } + + return "Pada hari $dayOfWeek, Anda cenderung mengalami $severityLabel " + + "ketika melakukan aktivitas $suggestedAktivitas dengan emosi $suggestedEmosi." + } + + // Fungsi untuk mendapatkan rekomendasi aktivitas + fun getRecommendedActivity(): String { + return when (predictedSeverity) { + "Minimal" -> "Pertahankan aktivitas seperti ${suggestedAktivitas.lowercase()} dan coba aktivitas baru yang menyenangkan." + "Ringan" -> "Pertimbangkan melakukan aktivitas relaksasi seperti yoga atau meditasi di samping ${suggestedAktivitas.lowercase()}." + "Sedang" -> "Disarankan untuk menyisihkan waktu istirahat yang cukup dan berbicara dengan teman dekat atau keluarga." + "Parah" -> "Sangat disarankan untuk berkonsultasi dengan profesional kesehatan mental dan kurangi beban aktivitas." + else -> "Jaga keseimbangan antara aktivitas dan waktu istirahat Anda." + } + } + + // Fungsi untuk prediksi yang lebih lengkap dengan rekomendasi + fun getCompleteDescription(): String { + return "${getReadableDescription()} ${getRecommendedActivity()}" + } + } + + // Function for backward compatibility + fun predictNextSevenDays( + lastDayNumber: Int, + detectionHistory: List + ): List> { + return predictNextSevenDaysDetail(lastDayNumber, detectionHistory) + .map { Pair(it.day, it.predictedSeverity) } + } + + private fun calculateGadScoreTrend(history: List): Double { + if (history.size < 2) return 0.0 + + // Simple linear regression for GAD score trend + val gadScores = history.map { it.totalSkor } + val x = (0 until gadScores.size).toList() + + val sumX = x.sum().toDouble() + val sumY = gadScores.sum().toDouble() + val sumXY = x.zip(gadScores).sumOf { (x, y) -> x * y }.toDouble() + val sumXX = x.sumOf { it * it }.toDouble() + val n = x.size + + return (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX) + } + + private fun calculatePredictedGadScore( + history: List, + trend: Double, + daysAhead: Int + ): Int { + if (history.isEmpty()) return 5 // Default value + + val lastGadScore = history.last().totalSkor + val predictedScore = lastGadScore + (trend * daysAhead) + + // Keep score within GAD-7 range (0-21) + return predictedScore.coerceIn(0.0, 21.0).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/core/strukturJSON_users_students b/app/src/main/java/com/example/app_skripsi/core/strukturJSON_users_students new file mode 100644 index 0000000..4d98dce --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/core/strukturJSON_users_students @@ -0,0 +1,51 @@ +{ + "users_students": { + "data": { + "userId_12345": { + "email": "user@example.com", + "jenis_kelamin": "Laki-laki", + "nama": "User", + "umur": 25, + "DeteksiRutin": { + "sesi_1": { + "aktif": true, + "deteksiHarian": { + "1": { + "emosi": "Sedih", + "kegiatan": "Olahraga", + "tanggal": "2025-03-01T12:00:00Z" + }, + "2": { + "emosi": "Senang", + "kegiatan": "Bekerja", + "tanggal": "2025-03-02T12:00:00Z" + } + }, + "periode": "1_bulan", + "tanggal_mulai": "2025-03-01T12:00:00Z", + "tanggal_selesai": "2025-03-31T12:00:00Z" + } + }, + "DeteksiSingkat": { + "deteksi_singkat_1": { + "emosi": "Marah", + "kegiatan": "Hiburan", + "tanggal": "2025-03-03T12:00:00Z", + "tingkat_kecemasan": 3 + } + }, + "Diary": { + "diary_1": { + "isiDiary": { + "emosi": "Senang", + "kegiatan": "Sosialisasi", + "judul": "Hari yang indah", + "isi": "Hari ini saya bertemu teman lama dan berbincang lama." + }, + "tanggal": "2025-03-03T12:00:00Z" + } + } + } + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/customview/CustomButton.kt b/app/src/main/java/com/example/app_skripsi/customview/CustomButton.kt new file mode 100644 index 0000000..72fc3b8 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/customview/CustomButton.kt @@ -0,0 +1,20 @@ +package com.example.app_skripsi.customview + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatButton +import androidx.core.content.ContextCompat +import com.example.app_skripsi.R + + +class CustomButton(context: Context, attrs: AttributeSet) : AppCompatButton(context, attrs) { + init { + setBackgroundResource(R.drawable.selector_button_background) + setTextColor(ContextCompat.getColor(context, R.color.white)) + textSize = 16f + typeface = resources.getFont(R.font.inter_bold) + isEnabled = false // Default disabled + isClickable = true // ⬅️ Aktifkan klik + isFocusable = true // ⬅️ Aktifkan fokus + } +} diff --git a/app/src/main/java/com/example/app_skripsi/customview/CustomTextField.kt b/app/src/main/java/com/example/app_skripsi/customview/CustomTextField.kt new file mode 100644 index 0000000..4d69cfc --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/customview/CustomTextField.kt @@ -0,0 +1,23 @@ +package com.example.app_skripsi.customview + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.widget.TextView +import androidx.appcompat.widget.AppCompatEditText +import com.example.app_skripsi.R +import com.google.android.material.textfield.TextInputEditText +import org.w3c.dom.Attr + + +class CustomTextField(context: Context, attr: AttributeSet) : TextInputEditText(context, attr) { + + init { + setPadding(40, 40, 40, 40) // ✅ Padding untuk kenyamanan input + setTextColor(resources.getColor(R.color.gray800, null)) + setHintTextColor(resources.getColor(R.color.gray500, null)) + textSize = 16f + typeface = resources.getFont(R.font.inter_regular) + setBackgroundResource(R.drawable.textfield_outline_background) // ✅ Background outline + } +} diff --git a/app/src/main/java/com/example/app_skripsi/data/firebase/AuthService.kt b/app/src/main/java/com/example/app_skripsi/data/firebase/AuthService.kt new file mode 100644 index 0000000..02b70f8 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/firebase/AuthService.kt @@ -0,0 +1,41 @@ +package com.example.app_skripsi.data.firebase + +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import kotlinx.coroutines.tasks.await + +class AuthService(private val auth: FirebaseAuth = FirebaseAuth.getInstance()) { + fun getCurrentUser(): FirebaseUser? = auth.currentUser + + suspend fun registerUser(email: String, password: String): Result { + return try { + val result = auth.createUserWithEmailAndPassword(email, password).await() + Result.success(result.user) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun loginUser(email: String, password: String): Result { + return try { + val result = auth.signInWithEmailAndPassword(email, password).await() + Result.success(result.user) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun sendPasswordResetEmail(email: String): Result { + return try { + FirebaseAuth.getInstance().sendPasswordResetEmail(email).await() + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + + fun logoutUser() { + auth.signOut() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/firebase/FirebaseService.kt b/app/src/main/java/com/example/app_skripsi/data/firebase/FirebaseService.kt new file mode 100644 index 0000000..b00780c --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/firebase/FirebaseService.kt @@ -0,0 +1,628 @@ +package com.example.app_skripsi.data.firebase + +import android.util.Log +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.DailyDetectionData +import com.example.app_skripsi.data.model.DiaryModel +import com.example.app_skripsi.data.model.RoutineDetectionModel +import com.example.app_skripsi.data.model.ShortDetectionModel +import com.example.app_skripsi.data.model.UserModel +import com.example.app_skripsi.data.model.VideoModel +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.tasks.await +import java.util.Calendar + +class FirebaseService( + private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance(), + private val auth: FirebaseAuth = FirebaseAuth.getInstance() +) { + init { + android.util.Log.d("FirebaseService", "🔥 FirebaseService Initialized") + } + + /** 🔹 Register User dengan menyimpan ke Firestore */ + suspend fun registerUser(email: String, password: String, user: UserModel): Result { + return try { + val authResult = auth.createUserWithEmailAndPassword(email, password).await() + val userId = authResult.user?.uid ?: return Result.failure(Exception("Gagal mendapatkan userId")) + + // 🔹 Path user dalam Firestore + val userRef = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + + // 🔹 Simpan data user ke Firestore + userRef.set(user.copy(email = email)).await() + + // 🔥 Buat koleksi kosong untuk DeteksiRutin, DeteksiSingkat, dan Diary + val initialData = hashMapOf("initialized" to true) // Placeholder untuk buat collection + + firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + .document("init").set(initialData).await() + + firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiSingkat") + .document("init").set(initialData).await() + + firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("Diary") + .document("init").set(initialData).await() + + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + + /** 🔹 Login User */ + suspend fun loginUser(email: String, password: String): Result { + return try { + auth.signInWithEmailAndPassword(email, password).await() + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Mengirim Email Reset Password */ + suspend fun sendPasswordResetEmail(email: String): Result { + return try { + auth.sendPasswordResetEmail(email).await() + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + + /** 🔹 Logout User */ + suspend fun logoutUser() { + auth.signOut() + } + + /** 🔹 Dapatkan User ID Saat Ini */ + suspend fun getCurrentUserId(): String? { + return auth.currentUser?.uid + } + + suspend fun getUserData(userId: String): Result { + return try { + val snapshot = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .get().await() + + val userData = snapshot.toObject(UserModel::class.java) + Result.success(userData) + } catch (e: Exception) { + Result.failure(e) + } + } + + + suspend fun addDiary(userId: String, diaryData: DiaryModel): Result { + return try { + val diaryCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("Diary") + + // Generate diaryId dengan format "diary_1", "diary_2", ... + val diaryCount = diaryCollection.get().await().size() + val diaryId = "diary_${diaryCount + 1}" + + diaryCollection.document(diaryId).set(diaryData).await() + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun getDiaries(userId: String): Result> { + return try { + val diaryCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("Diary") + + val snapshot = diaryCollection.get().await() + + // Filter untuk mengabaikan dokumen "init" + val diaryList = snapshot.documents + .filterNot { it.id == "init" } + .mapNotNull { it.toObject(DiaryModel::class.java) } + + Result.success(diaryList) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun addShortDetection(userId: String, detectionData: ShortDetectionModel): Result { + return try { + val detectionCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiSingkat") + + // Generate detectionId dengan format "deteksi_singkat_1", "deteksi_singkat_2", ... + val detectionCount = detectionCollection.get().await().size() + val detectionId = "deteksi_singkat_${detectionCount + 1}" + + detectionCollection.document(detectionId).set(detectionData).await() + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun getShortDetections(userId: String): Result> { + return try { + val detectionCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiSingkat") + + val snapshot = detectionCollection.get().await() + + // Filter untuk mengabaikan dokumen "init" + val detectionList = snapshot.documents + .filterNot { it.id == "init" } + .mapNotNull { it.toObject(ShortDetectionModel::class.java) } + + Result.success(detectionList) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Menambahkan Deteksi Rutin Baru */ + suspend fun addRoutineDetection(userId: String, routineData: RoutineDetectionModel): Result { + return try { + val routineCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + + // Generate detectionId dengan format "deteksi_rutin_1", "deteksi_rutin_2", ... + val detectionCount = routineCollection.get().await() + .documents.filterNot { it.id == "init" }.size + val detectionId = "deteksi_rutin_${detectionCount + 1}" + + routineCollection.document(detectionId).set(routineData).await() + Result.success(detectionId) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Mendapatkan Semua Deteksi Rutin */ + suspend fun getRoutineDetections(userId: String): Result>> { + return try { + val routineCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + + val snapshot = routineCollection.get().await() + + // Filter untuk mengabaikan dokumen "init" + val detectionList = snapshot.documents + .filterNot { it.id == "init" } + .mapNotNull { + val model = it.toObject(RoutineDetectionModel::class.java) + if (model != null) Pair(it.id, model) else null + } + + Result.success(detectionList) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Mendapatkan Deteksi Rutin yang Aktif */ + suspend fun getActiveRoutineDetection(userId: String): Result?> { + return try { + val routineCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + + val snapshot = routineCollection.whereEqualTo("aktif", true).get().await() + + // Mengambil deteksi rutin aktif pertama yang ditemukan + val activeDetection = snapshot.documents + .filterNot { it.id == "init" } + .firstOrNull() + ?.let { + val model = it.toObject(RoutineDetectionModel::class.java) + if (model != null) Pair(it.id, model) else null + } + + Result.success(activeDetection) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Menambahkan Deteksi Rutin Baru (dengan data hari pertama) */ + suspend fun createRoutineDetectionWithFirstDay( + userId: String, + routineData: RoutineDetectionModel + ): Result { + return try { + Log.d("FirebaseService", "Membuat deteksi rutin baru dengan data hari pertama") + + val routineCollection = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + + // Generate detectionId dengan format "deteksi_rutin_1", "deteksi_rutin_2", ... + val detectionCount = routineCollection.get().await() + .documents.filterNot { it.id == "init" }.size + val detectionId = "deteksi_rutin_${detectionCount + 1}" + + // Log untuk debugging + Log.d("FirebaseService", "Menyimpan deteksi rutin dengan ID: $detectionId") + Log.d("FirebaseService", "Data hari pertama: ${routineData.deteksiHarian["1"]?.emosi}") + + // Simpan dokumen baru + routineCollection.document(detectionId).set(routineData).await() + + // Verifikasi dokumen tersimpan + val verifyDoc = routineCollection.document(detectionId).get().await() + if (verifyDoc.exists()) { + Log.d("FirebaseService", "Dokumen berhasil dibuat dan terverifikasi") + } else { + Log.e("FirebaseService", "Dokumen gagal dibuat") + } + + Result.success(detectionId) + } catch (e: Exception) { + Log.e("FirebaseService", "Error membuat deteksi rutin: ${e.message}", e) + Result.failure(e) + } + } + + /** 🔹 Menambahkan Data Harian ke Deteksi Rutin yang Ada */ + suspend fun updateRoutineDetectionDailyData( + userId: String, + routineDocId: String, + dayNumber: Int, + dailyData: DailyDetectionData + ): Result { + return try { + Log.d("FirebaseService", "Mengupdate deteksi rutin $routineDocId untuk hari ke-$dayNumber") + + // Referensi dokumen + val routineRef = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + .document(routineDocId) + + // Verifikasi dokumen ada sebelum transaksi + val docCheck = routineRef.get().await() + if (!docCheck.exists()) { + Log.e("FirebaseService", "Dokumen $routineDocId tidak ditemukan!") + return Result.failure(Exception("Dokumen deteksi rutin tidak ditemukan")) + } + + // Ambil model saat ini untuk debugging + val currentModel = docCheck.toObject(RoutineDetectionModel::class.java) + Log.d("FirebaseService", "SEBELUM UPDATE: Jumlah data harian: ${currentModel?.deteksiHarian?.size}, Key: ${currentModel?.deteksiHarian?.keys}") + + // Jalankan transaksi update + firestore.runTransaction { transaction -> + // Ambil data terbaru + val snapshot = transaction.get(routineRef) + val routineData = snapshot.toObject(RoutineDetectionModel::class.java) + ?: throw Exception("Gagal konversi dokumen ke RoutineDetectionModel") + + // Buat Map baru dengan semua data yang ada + data hari ini + val updatedDailyData = HashMap() + updatedDailyData.putAll(routineData.deteksiHarian) + updatedDailyData[dayNumber.toString()] = dailyData + + Log.d("FirebaseService", "Jumlah data saat ini: ${routineData.deteksiHarian.size}") + Log.d("FirebaseService", "Jumlah data setelah update: ${updatedDailyData.size}") + Log.d("FirebaseService", "Hari baru: $dayNumber") + + // Update koleksi data harian + transaction.update(routineRef, "deteksiHarian", updatedDailyData) + + // Update skor tertinggi/terendah jika perlu + val currentScore = dailyData.totalSkor + val dayOfWeek = getDayName() + + if (routineData.skorTinggi < currentScore) { + transaction.update(routineRef, "skorTinggi", currentScore) + transaction.update(routineRef, "hariSkorTinggi", dayOfWeek) + } + + if (routineData.skorRendah > currentScore || routineData.skorRendah == 0) { + transaction.update(routineRef, "skorRendah", currentScore) + transaction.update(routineRef, "hariSkorRendah", dayOfWeek) + } + }.await() + + // Verifikasi update berhasil + val verifiedDoc = routineRef.get().await().toObject(RoutineDetectionModel::class.java) + Log.d("FirebaseService", "SETELAH UPDATE: Jumlah data harian: ${verifiedDoc?.deteksiHarian?.size}, Key: ${verifiedDoc?.deteksiHarian?.keys}") + + // Cek apakah data untuk hari ini ada + val hasTodayData = verifiedDoc?.deteksiHarian?.containsKey(dayNumber.toString()) ?: false + Log.d("FirebaseService", "Verifikasi data hari $dayNumber ada: $hasTodayData") + + Result.success(Unit) + } catch (e: Exception) { + Log.e("FirebaseService", "Error mengupdate deteksi rutin: ${e.message}", e) + Result.failure(e) + } + } + + // Fungsi helper untuk mendapatkan nama hari ini + private fun getDayName(): String { + return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> "Senin" + Calendar.TUESDAY -> "Selasa" + Calendar.WEDNESDAY -> "Rabu" + Calendar.THURSDAY -> "Kamis" + Calendar.FRIDAY -> "Jumat" + Calendar.SATURDAY -> "Sabtu" + Calendar.SUNDAY -> "Minggu" + else -> "" + } + } + + /** 🔹 Menambahkan Data Harian ke Deteksi Rutin yang Ada */ + suspend fun addDailyDataToRoutineDetection( + userId: String, + routineDocId: String, + dayNumber: Int, + dailyData: DailyDetectionData + ): Result { + return try { + Log.d("FirebaseService", "Adding daily data to routine $routineDocId for day $dayNumber") + + // Get reference to the routine document + val routineRef = firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + .document(routineDocId) + + // In FirebaseService.addDailyDataToRoutineDetection +// Add before the transaction: + val beforeDoc = routineRef.get().await().toObject(RoutineDetectionModel::class.java) + Log.d("FirebaseService", "BEFORE UPDATE: Daily data size: ${beforeDoc?.deteksiHarian?.size}, Keys: ${beforeDoc?.deteksiHarian?.keys}") + +// After the transaction: + val afterDoc = routineRef.get().await().toObject(RoutineDetectionModel::class.java) + Log.d("FirebaseService", "AFTER UPDATE: Daily data size: ${afterDoc?.deteksiHarian?.size}, Keys: ${afterDoc?.deteksiHarian?.keys}") + + // Run a transaction to update the document atomically + firestore.runTransaction { transaction -> + // Get current document state + val snapshot = transaction.get(routineRef) + if (!snapshot.exists()) { + throw Exception("Routine document does not exist") + } + + val routineData = snapshot.toObject(RoutineDetectionModel::class.java) + ?: throw Exception("Could not convert document to RoutineDetectionModel") + + // Create updated daily data map + val updatedDailyData = HashMap() + updatedDailyData.putAll(routineData.deteksiHarian) + updatedDailyData[dayNumber.toString()] = dailyData + + // Log data for debugging + Log.d("FirebaseService", "Current daily data size: ${routineData.deteksiHarian.size}") + Log.d("FirebaseService", "Updated daily data size: ${updatedDailyData.size}") + Log.d("FirebaseService", "New day number: $dayNumber") + + + + // Update fields that need to be updated + transaction.update(routineRef, "deteksiHarian", updatedDailyData) + + // Update min/max scores if needed + val currentScore = dailyData.totalSkor + val dayOfWeek = when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> "Senin" + Calendar.TUESDAY -> "Selasa" + Calendar.WEDNESDAY -> "Rabu" + Calendar.THURSDAY -> "Kamis" + Calendar.FRIDAY -> "Jumat" + Calendar.SATURDAY -> "Sabtu" + Calendar.SUNDAY -> "Minggu" + else -> "" + } + + // Update high score if needed + if (routineData.skorTinggi < currentScore) { + transaction.update(routineRef, "skorTinggi", currentScore) + transaction.update(routineRef, "hariSkorTinggi", dayOfWeek) + } + + // Update low score if needed (only if it's the first entry or lower than current min) + if (routineData.skorRendah > currentScore || routineData.skorRendah == 0) { + transaction.update(routineRef, "skorRendah", currentScore) + transaction.update(routineRef, "hariSkorRendah", dayOfWeek) + } + }.await() + + // Verify the update was successful + val updatedDoc = routineRef.get().await().toObject(RoutineDetectionModel::class.java) + Log.d("FirebaseService", "After update - daily data size: ${updatedDoc?.deteksiHarian?.size ?: 0}") + + Result.success(Unit) + } catch (e: Exception) { + Log.e("FirebaseService", "Error updating routine document: ${e.message}", e) + Result.failure(e) + } + } + + /** 🔹 Mengubah Status Aktif Deteksi Rutin */ + suspend fun updateRoutineDetectionStatus(userId: String, routineDocId: String, isActive: Boolean): Result { + return try { + firestore.collection("users_students") + .document("data") + .collection("userId") + .document(userId) + .collection("DeteksiRutin") + .document(routineDocId) + .update("aktif", isActive) + .await() + + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Mendapatkan Semua Video */ + suspend fun getVideos(): Result> { + return try { + // Akses dokumen yoga langsung karena tidak ada kategori lain + val videoDocument = firestore.collection("video_store") + .document("yoga") + .get().await() + + val videoList = mutableListOf() + val videoData = videoDocument.get("yoga_video") as? Map<*, *> + + if (videoData != null) { + for ((key, value) in videoData) { + if (value is Map<*, *>) { + val videoMap = value + val title = videoMap["title"] as? String ?: "" + val desc = videoMap["desc"] as? String ?: "" + val linkVideo = videoMap["link_video"] as? String ?: "" + val linkSource = videoMap["link_source"] as? String ?: "" + val hasCopyright = videoMap["cr"] as? Boolean ?: false + + val video = VideoModel( + id = "$key", + title = title, + description = desc, + videoUrl = linkVideo, + sourceUrl = linkSource, + hasCopyright = hasCopyright, + thumbnailUrl = R.drawable.video_thumbnail // Gunakan thumbnail default + ) + + videoList.add(video) + } + } + } + + Log.d("FirebaseService", "Loaded ${videoList.size} videos from yoga collection") + Result.success(videoList) + } catch (e: Exception) { + Log.e("FirebaseService", "Error getting videos: ${e.message}", e) + Result.failure(e) + } + } + + /** 🔹 Menambahkan Dokumen ke Firestore (Bisa ID Manual atau Otomatis) */ + suspend fun addDocument(collection: String, documentId: String? = null, data: T): Result { + return try { + val collectionRef = firestore.collection(collection) + + if (documentId != null) { + // Jika ID manual diberikan + collectionRef.document(documentId).set(data!!).await() + } else { + // Jika ID otomatis + collectionRef.add(data!!).await() + } + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + /** 🔹 Mengambil Dokumen Spesifik dengan ID */ + suspend fun getDocumentById(collection: String, documentId: String, clazz: Class): Result { + return try { + val snapshot = firestore.collection(collection).document(documentId).get().await() + val data = snapshot.toObject(clazz) + Result.success(data) + } catch (e: Exception) { + Result.failure(e) + } + } + + + +// /** 🔹 Mendapatkan Video Berdasarkan Kategori */ +// suspend fun getVideosByCategory(category: String): Result> { +// return try { +// val videoDocument = firestore.collection("video_store") +// .document(category) +// .get().await() +// +// val videoList = mutableListOf() +// val videoData = videoDocument.get("yoga_video") as? Map<*, *> +// +// if (videoData != null) { +// for ((key, value) in videoData) { +// if (value is Map<*, *>) { +// val videoMap = value +// val title = videoMap["title"] as? String ?: "" +// val desc = videoMap["desc"] as? String ?: "" +// val linkVideo = videoMap["link_video"] as? String ?: "" +// val linkSource = videoMap["link_source"] as? String ?: "" +// val hasCopyright = videoMap["cr"] as? Boolean ?: false +// +// val video = VideoModel( +// id = "$category-$key", +// title = title, +// description = desc, +// videoUrl = linkVideo, +// sourceUrl = linkSource, +// hasCopyright = hasCopyright, +// category = category +// ) +// +// videoList.add(video) +// } +// } +// } +// +// Result.success(videoList) +// } catch (e: Exception) { +// Log.e("FirebaseService", "Error getting videos by category: ${e.message}", e) +// Result.failure(e) +// } +// } + + +} diff --git a/app/src/main/java/com/example/app_skripsi/data/local/AppDatabase.kt b/app/src/main/java/com/example/app_skripsi/data/local/AppDatabase.kt new file mode 100644 index 0000000..5a793d5 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/local/AppDatabase.kt @@ -0,0 +1,33 @@ +package com.example.app_skripsi.data.local + + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.example.app_skripsi.data.local.user.UserDao +import com.example.app_skripsi.data.local.user.UserEntity + +@Database(entities = [UserEntity::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + + abstract fun userDao(): UserDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "app_database" + ).build() + INSTANCE = instance + android.util.Log.d("SQLite", "📦 SQLite Database Initialized") + instance + } + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/data/local/FormSessionManager.kt b/app/src/main/java/com/example/app_skripsi/data/local/FormSessionManager.kt new file mode 100644 index 0000000..6815329 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/local/FormSessionManager.kt @@ -0,0 +1,185 @@ +package com.example.app_skripsi.data.local + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +private val Context.formDataStore: DataStore by preferencesDataStore(name = "form_session") + +class FormSessionManager(private val context: Context) { + + // Keys untuk DataStore + companion object { + private val IS_SESSION_ACTIVE = booleanPreferencesKey("is_session_active") + private val EMOTION = stringPreferencesKey("emotion") + private val ACTIVITY = stringPreferencesKey("activity") + private val CURRENT_STEP = stringPreferencesKey("current_step") + + // Keys untuk pertanyaan GAD-7 + private val GAD_PREFIX = "gad_answer_" + private val GAD_TOTAL_SCORE = intPreferencesKey("gad_total_score") + + private val DETECTION_TYPE = stringPreferencesKey("detection_type") + } + + // Status sesi aktif + val isSessionActive: Flow = context.formDataStore.data.map { preferences -> + preferences[IS_SESSION_ACTIVE] ?: false + } + + // Emosi yang dipilih + val emotion: Flow = context.formDataStore.data.map { preferences -> + preferences[EMOTION] ?: "" + } + + // Aktivitas yang dipilih + val activity: Flow = context.formDataStore.data.map { preferences -> + preferences[ACTIVITY] ?: "" + } + + // Langkah saat ini + val currentStep: Flow = context.formDataStore.data.map { preferences -> + preferences[CURRENT_STEP] ?: "permission" // Default ke step pertama + } + + // Total skor GAD-7 + val gadTotalScore: Flow = context.formDataStore.data.map { preferences -> + preferences[GAD_TOTAL_SCORE] ?: 0 + } + + // Flow untuk tipe deteksi + val detectionType: Flow = context.formDataStore.data + .map { preferences -> preferences[DETECTION_TYPE] ?: "QUICK" } + + // Memulai sesi baru + suspend fun startSession() { + context.formDataStore.edit { preferences -> + preferences[IS_SESSION_ACTIVE] = true + preferences[CURRENT_STEP] = "permission" + } + } + + // Menyimpan pilihan emosi + suspend fun saveEmotion(selectedEmotion: String) { + context.formDataStore.edit { preferences -> + preferences[EMOTION] = selectedEmotion + preferences[CURRENT_STEP] = "emotion" + } + } + + // Menyimpan pilihan aktivitas + suspend fun saveActivity(selectedActivity: String) { + context.formDataStore.edit { preferences -> + preferences[ACTIVITY] = selectedActivity + preferences[CURRENT_STEP] = "activity" + } + } + // Method to get emotion as a suspend function + suspend fun getEmotion(): String { + return emotion.first() + } + + // Method to get activity as a suspend function + suspend fun getActivity(): String { + return activity.first() + } + + // Menyimpan jawaban GAD + suspend fun saveGadAnswer(questionNumber: Int, answerValue: Int) { + if (questionNumber in 0..6) { + val key = stringPreferencesKey("$GAD_PREFIX$questionNumber") // Ubah ke stringPreferencesKey + context.formDataStore.edit { preferences -> + preferences[key] = answerValue.toString() // Simpan sebagai string + preferences[CURRENT_STEP] = "gad_$questionNumber" + } + } + } + + // Mendapatkan jawaban GAD + suspend fun getGadAnswer(questionNumber: Int): Int { + if (questionNumber in 0..6) { + val key = stringPreferencesKey("$GAD_PREFIX$questionNumber") + val preferences = context.formDataStore.data.first() + val strValue = preferences[key] + + Log.d("FormSessionManager", "Mengambil jawaban untuk pertanyaan $questionNumber. Raw value: $strValue") + + return try { + val value = strValue?.toInt() ?: -1 + Log.d("FormSessionManager", "Jawaban pertanyaan $questionNumber = $value") + value + } catch (e: NumberFormatException) { + Log.e("FormSessionManager", "Error parsing jawaban GAD $questionNumber: $strValue", e) + -1 + } + } + return -1 + } + + // Menyimpan total skor GAD + suspend fun saveGadTotalScore(totalScore: Int) { + context.formDataStore.edit { preferences -> + preferences[GAD_TOTAL_SCORE] = totalScore + preferences[CURRENT_STEP] = "gad_completed" + } + } + + // Reset sesi + suspend fun resetSession() { + context.formDataStore.edit { preferences -> + // Hapus seluruh data untuk memastikan benar-benar bersih + preferences.clear() + + // Hapus secara spesifik kunci-kunci penting + preferences[IS_SESSION_ACTIVE] = false + preferences[CURRENT_STEP] = "permission" + preferences[EMOTION] = "" + preferences[ACTIVITY] = "" + preferences[GAD_TOTAL_SCORE] = 0 + preferences[DETECTION_TYPE] = "QUICK" // Reset ke tipe default + + // Hapus semua jawaban GAD dengan eksplisit + for (i in 0..6) { + val key = stringPreferencesKey("$GAD_PREFIX$i") + preferences.remove(key) + } + } + + // Cek apakah reset berhasil + val testPref = context.formDataStore.data.first() + Log.d("FormSessionManager", "Reset session - active: ${testPref[IS_SESSION_ACTIVE]}, step: ${testPref[CURRENT_STEP]}") + } + + // Tambahkan di FormSessionManager + suspend fun validateAllGadAnswers(): Boolean { + for (i in 0..6) { + val answer = getGadAnswer(i) + if (answer < 0) { + return false + } + } + return true + } + + // Simpan tipe deteksi + suspend fun saveDetectionType(type: String) { + context.formDataStore.edit { preferences -> + preferences[DETECTION_TYPE] = type + } + } + + // Dapatkan tipe deteksi + suspend fun getDetectionType(): String { + return context.formDataStore.data.first()[DETECTION_TYPE] ?: "QUICK" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/local/RoutineSessionManager.kt b/app/src/main/java/com/example/app_skripsi/data/local/RoutineSessionManager.kt new file mode 100644 index 0000000..4776549 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/local/RoutineSessionManager.kt @@ -0,0 +1,556 @@ +package com.example.app_skripsi.data.local + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.example.app_skripsi.data.firebase.FirebaseService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + + +// Extension function untuk DataStore +private val Context.routineSessionDataStore: DataStore by preferencesDataStore( + name = "routine_session_preferences" +) + +class RoutineSessionManager(private val context: Context) { + // Keys untuk DataStore + companion object { + private val SESSION_ACTIVE = booleanPreferencesKey("session_active") + private val SESSION_TYPE = stringPreferencesKey("session_type") // "1_WEEK", "2_WEEKS", "1_MONTH" + private val SESSION_START_DATE = longPreferencesKey("session_start_date") + private val SESSION_END_DATE = longPreferencesKey("session_end_date") + private val LAST_FORM_COMPLETION_DATE = stringPreferencesKey("last_form_completion_date") + private val USER_ID = stringPreferencesKey("user_id") + } + + // Flow untuk memantau status sesi + val isSessionActive: Flow = context.routineSessionDataStore.data + .map { preferences -> preferences[SESSION_ACTIVE] ?: false } + + // Flow untuk memantau tipe sesi + val sessionType: Flow = context.routineSessionDataStore.data + .map { preferences -> preferences[SESSION_TYPE] ?: "" } + + // Flow untuk tanggal mulai sesi + val sessionStartDate: Flow = context.routineSessionDataStore.data + .map { preferences -> preferences[SESSION_START_DATE] ?: 0L } + + // Flow untuk tanggal akhir sesi + val sessionEndDate: Flow = context.routineSessionDataStore.data + .map { preferences -> preferences[SESSION_END_DATE] ?: 0L } + + // Flow untuk tanggal terakhir pengisian form + val lastFormCompletionDate: Flow = context.routineSessionDataStore.data + .map { preferences -> preferences[LAST_FORM_COMPLETION_DATE] ?: "" } + + // Di kelas RoutineSessionManager, tambahkan fungsi berikut: + + // Method untuk mengakhiri sesi deteksi rutin secara menyeluruh (lokal dan Firebase) + suspend fun endRoutineSession(firebaseService: FirebaseService): Boolean { + try { + // Dapatkan user ID yang tersimpan + val userId = getUserId() ?: firebaseService.getCurrentUserId() + if (userId == null) { + Log.e("RoutineSessionManager", "User ID tidak tersedia") + return false + } + + Log.d("RoutineSessionManager", "Mengakhiri sesi untuk user: $userId") + + // Cari deteksi rutin yang aktif di Firebase + val result = firebaseService.getActiveRoutineDetection(userId) + + if (result.isSuccess) { + val activeRoutine = result.getOrNull() + + if (activeRoutine != null) { + // Update status di Firebase + Log.d("RoutineSessionManager", "Menonaktifkan dokumen rutin: ${activeRoutine.first}") + val updateResult = firebaseService.updateRoutineDetectionStatus( + userId, + activeRoutine.first, + false + ) + + if (updateResult.isSuccess) { + // Jika berhasil di Firebase, update juga DataStore + endSession() + Log.d("RoutineSessionManager", "Sesi rutin berhasil diakhiri") + return true + } else { + Log.e("RoutineSessionManager", "Gagal mengupdate status rutin di Firebase: ${updateResult.exceptionOrNull()?.message}") + } + } else { + Log.d("RoutineSessionManager", "Tidak ada sesi rutin aktif di Firebase, hanya mengakhiri sesi lokal") + // Tidak ada deteksi rutin aktif di Firebase, tetap akhiri sesi lokal + endSession() + return true + } + } else { + Log.e("RoutineSessionManager", "Error mengambil sesi rutin aktif: ${result.exceptionOrNull()?.message}") + } + + return false + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error saat mengakhiri sesi rutin: ${e.message}") + return false + } + } + + // Method untuk menyimpan user ID + suspend fun setUserId(userId: String) { + context.routineSessionDataStore.edit { preferences -> + preferences[USER_ID] = userId + } + Log.d("RoutineSessionManager", "User ID telah disimpan: $userId") + } + + // Method untuk mendapatkan user ID tersimpan + suspend fun getUserId(): String? { + return context.routineSessionDataStore.data.first()[USER_ID] + } + + // Modifikasi startNewSession untuk menerima parameter userId + suspend fun startNewSession(sessionType: String, userId: String) { + val calendar = Calendar.getInstance() + val startDate = calendar.timeInMillis + + // Menghitung tanggal akhir berdasarkan tipe sesi + val endDate = when(sessionType) { + "1_WEEK" -> { + calendar.add(Calendar.DAY_OF_YEAR, 7) + calendar.timeInMillis + } + "2_WEEKS" -> { + calendar.add(Calendar.DAY_OF_YEAR, 14) + calendar.timeInMillis + } + "1_MONTH" -> { + calendar.add(Calendar.MONTH, 1) + calendar.timeInMillis + } + else -> { + calendar.add(Calendar.DAY_OF_YEAR, 7) // Default 1 minggu + calendar.timeInMillis + } + } + + context.routineSessionDataStore.edit { preferences -> + preferences[SESSION_ACTIVE] = true + preferences[SESSION_TYPE] = sessionType + preferences[SESSION_START_DATE] = startDate + preferences[SESSION_END_DATE] = endDate + preferences[LAST_FORM_COMPLETION_DATE] = "" // Reset tanggal terakhir pengisian + preferences[USER_ID] = userId // Simpan user ID + } + + Log.d("RoutineSessionManager", "Sesi baru dimulai untuk user: $userId, tipe: $sessionType") + } + + + + // Method untuk validasi user + suspend fun validateUser(currentUserId: String): Boolean { + val storedUserId = getUserId() + return storedUserId == currentUserId + } + + // Method untuk sinkronisasi status dengan Firebase + suspend fun syncWithFirebase(firebaseService: FirebaseService) { + val userId = getUserId() ?: return + + try { + // Cek apakah data lokal menunjukkan sesi aktif + val isLocalActive = isSessionActive.first() + + if (isLocalActive) { + // Cek status di Firebase + val result = firebaseService.getActiveRoutineDetection(userId) + if (result.isSuccess) { + val hasActiveRoutine = result.getOrNull() != null + + // Jika di local active tapi di Firebase tidak ada yang active, + // update local menjadi tidak aktif + if (!hasActiveRoutine) { + endSession() + Log.d("RoutineSessionManager", "No active routine found in Firebase, ending local session") + } + } + } else { + // Local tidak aktif, pastikan tidak ada yang aktif di Firebase + val result = firebaseService.getActiveRoutineDetection(userId) + if (result.isSuccess && result.getOrNull() != null) { + // Ada yang aktif di Firebase tapi tidak di local, update Firebase + val activeRoutine = result.getOrNull() + if (activeRoutine != null) { + firebaseService.updateRoutineDetectionStatus(userId, activeRoutine.first, false) + Log.d("RoutineSessionManager", "Found active routine in Firebase but local is inactive, deactivating in Firebase") + } + } + } + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error syncing with Firebase: ${e.message}") + } + } + + // Method untuk me-reset semua data saat logout + suspend fun clearAllData() { + context.routineSessionDataStore.edit { preferences -> + preferences.clear() + } + Log.d("RoutineSessionManager", "All routine session data cleared") + } + + + + // Method untuk pengecekan apakah hari ini adalah hari terakhir sesi + suspend fun isTodayLastDayOfSession(): Boolean { + val isActive = isSessionStillActive() + if (!isActive) return false + + val preferences = context.routineSessionDataStore.data.first() + val endDate = preferences[SESSION_END_DATE] ?: 0L + + // Buat kalender untuk tanggal akhir dan hari ini + val endCalendar = Calendar.getInstance() + endCalendar.timeInMillis = endDate + endCalendar.set(Calendar.HOUR_OF_DAY, 0) + endCalendar.set(Calendar.MINUTE, 0) + endCalendar.set(Calendar.SECOND, 0) + endCalendar.set(Calendar.MILLISECOND, 0) + + val todayCalendar = Calendar.getInstance() + todayCalendar.set(Calendar.HOUR_OF_DAY, 0) + todayCalendar.set(Calendar.MINUTE, 0) + todayCalendar.set(Calendar.SECOND, 0) + todayCalendar.set(Calendar.MILLISECOND, 0) + + // Cek apakah tanggal sama + return endCalendar.timeInMillis == todayCalendar.timeInMillis + } + + // Fungsi untuk mengakhiri sesi di DataStore dan Firebase + suspend fun endRoutineSessionCompletely(firebaseService: FirebaseService): Boolean { + try { + val userId = getUserId() ?: return false + + // Dapatkan dokumen rutin yang aktif + val result = firebaseService.getActiveRoutineDetection(userId) + if (result.isSuccess) { + val activeDetection = result.getOrNull() + if (activeDetection != null) { + // Update status di Firebase + val updateResult = firebaseService.updateRoutineDetectionStatus( + userId, + activeDetection.first, + false + ) + + if (updateResult.isSuccess) { + // Update local storage + endSession() + Log.d("RoutineSessionManager", "Session ended successfully") + return true + } + } + } + + return false + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error ending routine session", e) + return false + } + } + + // Memulai sesi baru + suspend fun startNewSession(sessionType: String) { + val calendar = Calendar.getInstance() + val startDate = calendar.timeInMillis + + // Menghitung tanggal akhir berdasarkan tipe sesi + val endDate = when(sessionType) { + "1_WEEK" -> { + calendar.add(Calendar.DAY_OF_YEAR, 7) + calendar.timeInMillis + } + "2_WEEKS" -> { + calendar.add(Calendar.DAY_OF_YEAR, 14) + calendar.timeInMillis + } + "1_MONTH" -> { + calendar.add(Calendar.MONTH, 1) + calendar.timeInMillis + } + else -> { + calendar.add(Calendar.DAY_OF_YEAR, 7) // Default 1 minggu + calendar.timeInMillis + } + } + + context.routineSessionDataStore.edit { preferences -> + preferences[SESSION_ACTIVE] = true + preferences[SESSION_TYPE] = sessionType + preferences[SESSION_START_DATE] = startDate + preferences[SESSION_END_DATE] = endDate + preferences[LAST_FORM_COMPLETION_DATE] = "" // Reset tanggal terakhir pengisian + } + } + + suspend fun saveFormCompletionForToday() { + val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + Log.d("RoutineSessionManager", "Saving form completion for date: $today") + + try { + context.routineSessionDataStore.edit { preferences -> + preferences[LAST_FORM_COMPLETION_DATE] = today + } + + // Verifikasi penyimpanan + val savedDate = context.routineSessionDataStore.data.first()[LAST_FORM_COMPLETION_DATE] ?: "" + Log.d("RoutineSessionManager", "Verification - Saved completion date: $savedDate") + + if (savedDate != today) { + Log.e("RoutineSessionManager", "ERROR: Failed to save completion date! Expected: $today, Actual: $savedDate") + } + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error saving form completion date", e) + } + } + + suspend fun hasCompletedFormToday(): Boolean { + val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + val preferences = context.routineSessionDataStore.data.first() + val lastDate = preferences[LAST_FORM_COMPLETION_DATE] ?: "" + + val result = lastDate == today + Log.d("RoutineSessionManager", "Checking form completion - Today: $today, Last completion: $lastDate, Result: $result") + + return result + } + + + + // Mengakhiri sesi + suspend fun endSession() { + context.routineSessionDataStore.edit { preferences -> + preferences[SESSION_ACTIVE] = false + } + } + + // Cek apakah sesi masih aktif + suspend fun isSessionStillActive(): Boolean { + val preferences = context.routineSessionDataStore.data.first() + val isActive = preferences[SESSION_ACTIVE] ?: false + val endDate = preferences[SESSION_END_DATE] ?: 0L + + if (!isActive) return false + + val currentTime = System.currentTimeMillis() + + // Jika tanggal saat ini melebihi tanggal akhir, sesi berakhir + if (currentTime > endDate) { + try { + // Akhiri sesi di DataStore + endSession() + + // Dapatkan userId + val userId = getUserId() + + // Jika userId tersedia, update juga di Firebase + if (userId != null) { + // Ini perlu ditambahkan di method ini atau di class yang menggunakannya + val firebaseService = FirebaseService() + val result = firebaseService.getActiveRoutineDetection(userId) + + if (result.isSuccess && result.getOrNull() != null) { + val activeRoutine = result.getOrNull()!! + firebaseService.updateRoutineDetectionStatus(userId, activeRoutine.first, false) + Log.d("RoutineSessionManager", "Routine session ended in Firestore due to end date") + } + } + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error ending session in Firestore: ${e.message}") + } + + return false + } + return true + } + + // Mendapatkan hari ke berapa dari sesi + suspend fun getCurrentSessionDay(): Int { + val preferences = context.routineSessionDataStore.data.first() + val startDate = preferences[SESSION_START_DATE] ?: 0L + + if (startDate == 0L) return 0 + + val currentTime = System.currentTimeMillis() + + val startCalendar = Calendar.getInstance() + startCalendar.timeInMillis = startDate + + val currentCalendar = Calendar.getInstance() + currentCalendar.timeInMillis = currentTime + + // Reset time component untuk akurasi perhitungan hari + startCalendar.set(Calendar.HOUR_OF_DAY, 0) + startCalendar.set(Calendar.MINUTE, 0) + startCalendar.set(Calendar.SECOND, 0) + startCalendar.set(Calendar.MILLISECOND, 0) + + currentCalendar.set(Calendar.HOUR_OF_DAY, 0) + currentCalendar.set(Calendar.MINUTE, 0) + currentCalendar.set(Calendar.SECOND, 0) + currentCalendar.set(Calendar.MILLISECOND, 0) + + val diffInMillis = currentCalendar.timeInMillis - startCalendar.timeInMillis + return (diffInMillis / (24 * 60 * 60 * 1000)).toInt() + 1 + } + + // Mendapatkan tipe sesi dalam format yang lebih ramah pengguna + suspend fun getSessionTypeDisplay(): String { + val preferences = context.routineSessionDataStore.data.first() + val typeValue = preferences[SESSION_TYPE] ?: "" + + return when(typeValue) { + "1_WEEK" -> "1 Minggu" + "2_WEEKS" -> "2 Minggu" + "1_MONTH" -> "1 Bulan" + else -> "Tidak diketahui" + } + } + + // Mendapatkan durasi sesi dalam hari + suspend fun getSessionDurationInDays(): Int { + val preferences = context.routineSessionDataStore.data.first() + val typeValue = preferences[SESSION_TYPE] ?: "" + + return when(typeValue) { + "1_WEEK" -> 7 + "2_WEEKS" -> 14 + "1_MONTH" -> 30 + else -> 7 // default 1 minggu + } + } + + suspend fun forceEndCurrentSession(): Boolean { + try { + // Update local DataStore + context.routineSessionDataStore.edit { preferences -> + preferences[SESSION_ACTIVE] = false + } + + Log.d("RoutineSessionManager", "Session successfully ended in DataStore") + return true + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error ending session", e) + return false + } + } + + // Get document ID for current routine session + suspend fun getCurrentRoutineDocumentId(): String? { + val isActive = isSessionStillActive() + if (!isActive) return null + + // Menggunakan startDate sebagai identifier + val startDate = sessionStartDate.first() + return "deteksi_rutin_${startDate}" + } + + + // Tambahkan fungsi untuk cek apakah semua hari dalam periode sudah diisi + suspend fun getCompletedSessionProgress(firebaseService: FirebaseService): Pair { + val userId = firebaseService.getCurrentUserId() ?: return Pair(0, 0) + + try { + // Dapatkan dokumen deteksi rutin yang aktif + val result = firebaseService.getActiveRoutineDetection(userId) + if (result.isSuccess) { + val activeDetection = result.getOrNull() + if (activeDetection != null) { + val totalDays = getSessionDurationInDays() + val completedDays = activeDetection.second.deteksiHarian.size + return Pair(completedDays, totalDays) + } + } + + return Pair(0, 0) + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error getting session progress", e) + return Pair(0, 0) + } + } + + // Tambahkan di RoutineSessionManager.kt + suspend fun restoreSessionFromFirebase(firebaseService: FirebaseService): Boolean { + try { + // Ambil user ID saat ini + val userId = firebaseService.getCurrentUserId() ?: return false + + // Simpan user ID ke DataStore + setUserId(userId) + + // Cek apakah ada sesi aktif di Firebase + val result = firebaseService.getActiveRoutineDetection(userId) + + if (result.isSuccess && result.getOrNull() != null) { + val activeRoutine = result.getOrNull()!! + + // Ada sesi aktif, restore data ke DataStore + val sessionType = activeRoutine.second.periode + val startDate = activeRoutine.second.tanggalMulai.toDate().time + val endDate = activeRoutine.second.tanggalSelesai.toDate().time + + // Hitung progress sesi (untuk completion date) + val dayEntries = activeRoutine.second.deteksiHarian + + // Ambil tanggal sesi terbaru jika ada + val latestDateEntry = dayEntries.entries + .maxByOrNull { it.key.toIntOrNull() ?: 0 } + + val lastCompletionDate = if (latestDateEntry != null) { + val latestDetection = latestDateEntry.value + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + .format(latestDetection.tanggal.toDate()) + } else { + "" + } + + // Restore data ke DataStore + context.routineSessionDataStore.edit { preferences -> + preferences[SESSION_ACTIVE] = true + preferences[SESSION_TYPE] = sessionType + preferences[SESSION_START_DATE] = startDate + preferences[SESSION_END_DATE] = endDate + preferences[LAST_FORM_COMPLETION_DATE] = lastCompletionDate + preferences[USER_ID] = userId + } + + Log.d("RoutineSessionManager", "Berhasil restore sesi dari Firebase: tipe=$sessionType, " + + "hari ke-${dayEntries.size}, completion date=$lastCompletionDate") + + return true + } else { + Log.d("RoutineSessionManager", "Tidak ada sesi aktif di Firebase untuk dipulihkan") + return false + } + } catch (e: Exception) { + Log.e("RoutineSessionManager", "Error merestorasi sesi: ${e.message}") + return false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/local/SessionManager.kt b/app/src/main/java/com/example/app_skripsi/data/local/SessionManager.kt new file mode 100644 index 0000000..6f3dd1a --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/local/SessionManager.kt @@ -0,0 +1,58 @@ +package com.example.app_skripsi.data.local + +import android.content.Context +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +// Inisialisasi DataStore +private val Context.dataStore by preferencesDataStore(name = "user_session") + +class SessionManager(private val context: Context) { + + companion object { + private val TOKEN_KEY = stringPreferencesKey("session_token") + private val USER_ID_KEY = stringPreferencesKey("user_id") + private val EXPIRES_AT_KEY = longPreferencesKey("expires_at") + } + + // Simpan session user + suspend fun saveSession(token: String, userId: String, expiresAt: Long) { + context.dataStore.edit { preferences -> + preferences[TOKEN_KEY] = token + preferences[USER_ID_KEY] = userId + preferences[EXPIRES_AT_KEY] = expiresAt + } + android.util.Log.d("SessionManager", "Session saved for userId: $userId, token: $token") + } + + + // Ambil token session + val sessionToken: Flow = context.dataStore.data.map { preferences -> + preferences[TOKEN_KEY] + } + + // Ambil user ID + val sessionUserId: Flow = context.dataStore.data.map { preferences -> + preferences[USER_ID_KEY] + } + + // Ambil waktu kadaluarsa session + val sessionExpiresAt: Flow = context.dataStore.data.map { preferences -> + preferences[EXPIRES_AT_KEY] + } + + // Hapus session saat logout + suspend fun clearSession() { + android.util.Log.e("SessionManager", "🗑 Clearing user session...") + context.dataStore.edit { preferences -> + preferences.remove(TOKEN_KEY) + preferences.remove(USER_ID_KEY) + preferences.remove(EXPIRES_AT_KEY) + + // 🔹 Cetak Log saat Logout + android.util.Log.d("SessionManager", "Token Cleared: User logged out") + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/data/local/user/UserDao.kt b/app/src/main/java/com/example/app_skripsi/data/local/user/UserDao.kt new file mode 100644 index 0000000..2e7bdba --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/local/user/UserDao.kt @@ -0,0 +1,22 @@ +package com.example.app_skripsi.data.local.user + +import androidx.room.* +import com.example.app_skripsi.data.local.user.UserEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface UserDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertUser(user: UserEntity) + +// @Query("SELECT * FROM users WHERE userId = :userId LIMIT 1") +// fun getUser(userId: String): Flow + + @Query("SELECT * FROM users WHERE userId = :userId LIMIT 1") + suspend fun getUserById(userId: String): UserEntity? // ✅ Add this function + + + @Query("DELETE FROM users") + suspend fun deleteAllUsers() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/local/user/UserEntity.kt b/app/src/main/java/com/example/app_skripsi/data/local/user/UserEntity.kt new file mode 100644 index 0000000..fed25f7 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/local/user/UserEntity.kt @@ -0,0 +1,24 @@ +package com.example.app_skripsi.data.local.user + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.example.app_skripsi.data.model.UserModel + +@Entity(tableName = "users") +data class UserEntity( + @PrimaryKey val userId: String, + val nama: String, + val email: String, + val jenisKelamin: String, + val umur: Int +) + +fun UserEntity.toUserModel(): UserModel { + return UserModel( + nama = this.nama, + email = this.email, + jenisKelamin = this.jenisKelamin, + umur = this.umur + ) +} + diff --git a/app/src/main/java/com/example/app_skripsi/data/model/DiaryModel.kt b/app/src/main/java/com/example/app_skripsi/data/model/DiaryModel.kt new file mode 100644 index 0000000..02cb00c --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/model/DiaryModel.kt @@ -0,0 +1,12 @@ +package com.example.app_skripsi.data.model +import com.google.firebase.firestore.IgnoreExtraProperties + +import com.google.firebase.Timestamp + +@IgnoreExtraProperties +data class DiaryModel( + var isiDiary: Map = emptyMap(), + var tanggal: Timestamp = Timestamp.now() +) { + constructor() : this(emptyMap(), Timestamp.now()) +} diff --git a/app/src/main/java/com/example/app_skripsi/data/model/FaqModel.kt b/app/src/main/java/com/example/app_skripsi/data/model/FaqModel.kt new file mode 100644 index 0000000..d6608e3 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/model/FaqModel.kt @@ -0,0 +1,5 @@ +package com.example.app_skripsi.data.model + +data class FaqModel(val question: String, + val answer: String, + var isExpanded: Boolean = false) diff --git a/app/src/main/java/com/example/app_skripsi/data/model/RoutineDetectionModel.kt b/app/src/main/java/com/example/app_skripsi/data/model/RoutineDetectionModel.kt new file mode 100644 index 0000000..7905a27 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/model/RoutineDetectionModel.kt @@ -0,0 +1,59 @@ +package com.example.app_skripsi.data.model + +import com.google.firebase.Timestamp +import com.google.firebase.firestore.PropertyName + +data class RoutineDetectionModel( + val aktif: Boolean = true, + + @get:PropertyName("deteksiHarian") + @set:PropertyName("deteksiHarian") + var deteksiHarian: Map = mapOf(), + + @get:PropertyName("hariSkorRendah") + @set:PropertyName("hariSkorRendah") + var hariSkorRendah: String = "", + + @get:PropertyName("hariSkorTinggi") + @set:PropertyName("hariSkorTinggi") + var hariSkorTinggi: String = "", + + @get:PropertyName("skorRendah") + @set:PropertyName("skorRendah") + var skorRendah: Int = 0, + + @get:PropertyName("skorTinggi") + @set:PropertyName("skorTinggi") + var skorTinggi: Int = 0, + + @get:PropertyName("periode") + @set:PropertyName("periode") + var periode: String = "", + + @get:PropertyName("tanggalMulai") + @set:PropertyName("tanggalMulai") + var tanggalMulai: Timestamp = Timestamp.now(), + + @get:PropertyName("tanggalSelesai") + @set:PropertyName("tanggalSelesai") + var tanggalSelesai: Timestamp = Timestamp.now() +) + +data class DailyDetectionData( + val emosi: String = "", + val kegiatan: String = "", + val gad1: Int = 0, + val gad2: Int = 0, + val gad3: Int = 0, + val gad4: Int = 0, + val gad5: Int = 0, + val gad6: Int = 0, + val gad7: Int = 0, + val tanggal: Timestamp = Timestamp.now(), + + @get:PropertyName("total_skor") + @set:PropertyName("total_skor") + var totalSkor: Int = 0, + + val severity: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/model/ShortDetectionModel.kt b/app/src/main/java/com/example/app_skripsi/data/model/ShortDetectionModel.kt new file mode 100644 index 0000000..560f0bb --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/model/ShortDetectionModel.kt @@ -0,0 +1,19 @@ +package com.example.app_skripsi.data.model + +import com.google.firebase.Timestamp + +data class ShortDetectionModel( + val emosi: String = "", + val kegiatan: String = "", + val tanggal: Timestamp = Timestamp.now(), + val hari: String = "", + val gad1: Int = 0, + val gad2: Int = 0, + val gad3: Int = 0, + val gad4: Int = 0, + val gad5: Int = 0, + val gad6: Int = 0, + val gad7: Int = 0, + val total_skor: Int = 0, + val severity: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/model/UserModel.kt b/app/src/main/java/com/example/app_skripsi/data/model/UserModel.kt new file mode 100644 index 0000000..2d17377 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/model/UserModel.kt @@ -0,0 +1,14 @@ +package com.example.app_skripsi.data.model + +import com.google.firebase.firestore.IgnoreExtraProperties + +@IgnoreExtraProperties +data class UserModel( + var nama: String = "", + var email: String = "", + var jenisKelamin: String = "", + var umur: Int = 0 +) { + // Constructor kosong dibutuhkan untuk Firebase Firestore + constructor() : this("", "", "", 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/model/VideoModel.kt b/app/src/main/java/com/example/app_skripsi/data/model/VideoModel.kt new file mode 100644 index 0000000..eb030f1 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/model/VideoModel.kt @@ -0,0 +1,16 @@ +package com.example.app_skripsi.data.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class VideoModel( + val id: String = "", + val title: String = "", + val description: String = "", + val videoUrl: String = "", + val thumbnailUrl: Int = 0, // Resource ID untuk thumbnail default + val sourceUrl: String = "", + val hasCopyright: Boolean = false, + val category: String = "" +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/repository/AnxietyRepository.kt b/app/src/main/java/com/example/app_skripsi/data/repository/AnxietyRepository.kt new file mode 100644 index 0000000..1487f78 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/repository/AnxietyRepository.kt @@ -0,0 +1,593 @@ +package com.example.app_skripsi.data.repository + +import android.util.Log +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.model.DailyDetectionData +import com.example.app_skripsi.data.model.RoutineDetectionModel +import com.example.app_skripsi.data.model.ShortDetectionModel +import com.google.firebase.Timestamp + +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit + + +class AnxietyRepository(private val firebaseService: FirebaseService) { + + // Fungsi deteksi singkat yang sudah ada + suspend fun addShortDetection( + emotion: String, + activity: String, + gadAnswers: List, + totalScore: Int + ): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + // Mendapatkan hari dan tanggal saat ini + val calendar = Calendar.getInstance() + val dayOfWeek = getDayOfWeek(calendar) + val currentDate = Date() + + // Menentukan tingkat keparahan berdasarkan total skor + val severity = getSeverityLevel(totalScore) + + // Membuat model data untuk deteksi singkat + val shortDetection = ShortDetectionModel( + emosi = emotion, + kegiatan = activity, + tanggal = Timestamp(currentDate), + hari = dayOfWeek, + gad1 = gadAnswers.getOrElse(0) { 0 }, + gad2 = gadAnswers.getOrElse(1) { 0 }, + gad3 = gadAnswers.getOrElse(2) { 0 }, + gad4 = gadAnswers.getOrElse(3) { 0 }, + gad5 = gadAnswers.getOrElse(4) { 0 }, + gad6 = gadAnswers.getOrElse(5) { 0 }, + gad7 = gadAnswers.getOrElse(6) { 0 }, + total_skor = totalScore, + severity = severity + ) + + return firebaseService.addShortDetection(userId, shortDetection) + + } catch (e: Exception) { + return Result.failure(e) + } + } + + suspend fun getShortDetections(): Result> { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + return firebaseService.getShortDetections(userId) + } + + /** + * Membuat dokumen deteksi rutin baru dengan data hari pertama + */ + suspend fun createNewRoutineWithFirstDay( + sessionType: String, + emotion: String, + activity: String, + gadAnswers: List, + totalScore: Int + ): Result { + val userId = firebaseService.getCurrentUserId() ?: + return Result.failure(Exception("User tidak login")) + + try { + Log.d("AnxietyRepository", "Membuat deteksi rutin baru dengan data hari pertama") + + // Setup tanggal + val startDate = Calendar.getInstance() + val endDate = Calendar.getInstance() + val currentDate = Date() + val dayOfWeek = getDayOfWeek(Calendar.getInstance()) + + // Set tanggal akhir + when(sessionType) { + "1_WEEK" -> endDate.add(Calendar.DATE, 7) + "2_WEEKS" -> endDate.add(Calendar.DATE, 14) + "1_MONTH" -> endDate.add(Calendar.DATE, 30) + else -> endDate.add(Calendar.DATE, 7) // Default 1 minggu + } + + // Buat data hari pertama + val dailyData = DailyDetectionData( + emosi = emotion, + kegiatan = activity, + gad1 = gadAnswers.getOrElse(0) { 0 }, + gad2 = gadAnswers.getOrElse(1) { 0 }, + gad3 = gadAnswers.getOrElse(2) { 0 }, + gad4 = gadAnswers.getOrElse(3) { 0 }, + gad5 = gadAnswers.getOrElse(4) { 0 }, + gad6 = gadAnswers.getOrElse(5) { 0 }, + gad7 = gadAnswers.getOrElse(6) { 0 }, + tanggal = Timestamp(currentDate), + totalSkor = totalScore, + severity = getSeverityLevel(totalScore) + ) + + // Buat model deteksi rutin dengan data hari pertama + val routineDetection = RoutineDetectionModel( + aktif = true, + deteksiHarian = mapOf("1" to dailyData), + hariSkorRendah = dayOfWeek, + hariSkorTinggi = dayOfWeek, + skorRendah = totalScore, + skorTinggi = totalScore, + periode = sessionType, + tanggalMulai = Timestamp(startDate.time), + tanggalSelesai = Timestamp(endDate.time) + ) + + // Simpan ke Firestore dengan method khusus + val result = firebaseService.createRoutineDetectionWithFirstDay(userId, routineDetection) + + if (result.isSuccess) { + Log.d("AnxietyRepository", "Berhasil membuat deteksi rutin dengan ID: ${result.getOrNull()}") + } else { + Log.e("AnxietyRepository", "Gagal membuat deteksi rutin: ${result.exceptionOrNull()?.message}") + } + + return result + } catch (e: Exception) { + Log.e("AnxietyRepository", "Error saat membuat deteksi rutin: ${e.message}", e) + return Result.failure(e) + } + } + + /** + * Menambahkan data hari ke-N ke deteksi rutin yang sudah ada + */ + suspend fun addDailyDataToExistingRoutine( + routineDocId: String, + emotion: String, + activity: String, + gadAnswers: List, + totalScore: Int, + routineSessionManager: RoutineSessionManager + ): Result { + val userId = firebaseService.getCurrentUserId() ?: + return Result.failure(Exception("User tidak login")) + + try { + // Dapatkan hari saat ini dalam sesi + val currentDayResult = getCurrentDayInRoutineSession(routineDocId) + if (currentDayResult.isFailure) { + return Result.failure(currentDayResult.exceptionOrNull()!!) + } + + val currentDay = currentDayResult.getOrNull() ?: + return Result.failure(Exception("Gagal mendapatkan hari saat ini")) + + Log.d("AnxietyRepository", "Menambahkan data untuk hari ke-$currentDay di deteksi rutin $routineDocId") + + // Buat data harian + val dailyData = DailyDetectionData( + emosi = emotion, + kegiatan = activity, + gad1 = gadAnswers.getOrElse(0) { 0 }, + gad2 = gadAnswers.getOrElse(1) { 0 }, + gad3 = gadAnswers.getOrElse(2) { 0 }, + gad4 = gadAnswers.getOrElse(3) { 0 }, + gad5 = gadAnswers.getOrElse(4) { 0 }, + gad6 = gadAnswers.getOrElse(5) { 0 }, + gad7 = gadAnswers.getOrElse(6) { 0 }, + tanggal = Timestamp(Date()), + totalSkor = totalScore, + severity = getSeverityLevel(totalScore) + ) + + // Update dengan method khusus + val result = firebaseService.updateRoutineDetectionDailyData( + userId, routineDocId, currentDay, dailyData + ) + + // Jika berhasil, tandai selesai di local storage + if (result.isSuccess) { + routineSessionManager.saveFormCompletionForToday() + + // Verifikasi status completion + val completionStatus = routineSessionManager.hasCompletedFormToday() + Log.d("AnxietyRepository", "Data hari ke-$currentDay berhasil disimpan. Status completion: $completionStatus") + + // Verifikasi data tersimpan di Firestore + val verifyResult = firebaseService.getRoutineDetections(userId) + if (verifyResult.isSuccess) { + val routineData = verifyResult.getOrNull()?.find { it.first == routineDocId }?.second + if (routineData != null) { + val hasDayData = routineData.deteksiHarian.containsKey(currentDay.toString()) + Log.d("AnxietyRepository", "Verifikasi di Firestore - data hari $currentDay ada: $hasDayData") + } + } + } else { + Log.e("AnxietyRepository", "Gagal menyimpan data hari ke-$currentDay: ${result.exceptionOrNull()?.message}") + } + + return result + } catch (e: Exception) { + Log.e("AnxietyRepository", "Error menambahkan data harian: ${e.message}", e) + return Result.failure(e) + } + } + + /** + * Mendapatkan hari saat ini dalam sesi dengan perhitungan yang lebih akurat + */ + suspend fun getCurrentDayInRoutineSession(routineDocId: String): Result { + val userId = firebaseService.getCurrentUserId() ?: + return Result.failure(Exception("User tidak login")) + + try { + val routineResult = firebaseService.getRoutineDetections(userId) + if (routineResult.isFailure) + return Result.failure(routineResult.exceptionOrNull()!!) + + val routineList = routineResult.getOrNull() + val routineData = routineList?.find { it.first == routineDocId }?.second + ?: return Result.failure(Exception("Deteksi rutin tidak ditemukan")) + + // Ambil tanggal mulai dari Firestore + val startDate = routineData.tanggalMulai.toDate() + val currentDate = Date() + + // Log untuk debugging + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + Log.d("AnxietyRepository", "Tanggal mulai sesi: ${dateFormat.format(startDate)}") + Log.d("AnxietyRepository", "Tanggal hari ini: ${dateFormat.format(currentDate)}") + + // Hitung selisih hari dengan reset komponen waktu + val startCalendar = Calendar.getInstance() + startCalendar.time = startDate + startCalendar.set(Calendar.HOUR_OF_DAY, 0) + startCalendar.set(Calendar.MINUTE, 0) + startCalendar.set(Calendar.SECOND, 0) + startCalendar.set(Calendar.MILLISECOND, 0) + + val currentCalendar = Calendar.getInstance() + currentCalendar.time = currentDate + currentCalendar.set(Calendar.HOUR_OF_DAY, 0) + currentCalendar.set(Calendar.MINUTE, 0) + currentCalendar.set(Calendar.SECOND, 0) + currentCalendar.set(Calendar.MILLISECOND, 0) + + val diffInMillis = currentCalendar.timeInMillis - startCalendar.timeInMillis + val diffInDays = (diffInMillis / (24 * 60 * 60 * 1000)).toInt() + + // Hari dalam sesi dimulai dari 1 + val dayInSession = diffInDays + 1 + Log.d("AnxietyRepository", "Hari dalam sesi: $dayInSession") + + return Result.success(dayInSession) + } catch (e: Exception) { + Log.e("AnxietyRepository", "Error menghitung hari saat ini: ${e.message}", e) + return Result.failure(e) + } + } + + /** + * Membuat sesi deteksi rutin baru + */ + suspend fun createRoutineDetection(sessionType: String): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + val startDate = Calendar.getInstance() + val endDate = Calendar.getInstance() + + // Menentukan tanggal berakhir berdasarkan tipe sesi + when(sessionType) { + "1_WEEK" -> endDate.add(Calendar.DATE, 7) + "2_WEEKS" -> endDate.add(Calendar.DATE, 14) + "1_MONTH" -> endDate.add(Calendar.DATE, 30) + else -> endDate.add(Calendar.DATE, 7) // Default 1 minggu + } + + // Membuat model deteksi rutin baru + val routineDetection = RoutineDetectionModel( + aktif = true, + deteksiHarian = mapOf(), // Kosong di awal + hariSkorRendah = "", + hariSkorTinggi = "", + skorRendah = 0, + skorTinggi = 0, + periode = sessionType, + tanggalMulai = Timestamp(startDate.time), + tanggalSelesai = Timestamp(endDate.time) + ) + + return firebaseService.addRoutineDetection(userId, routineDetection) + } catch (e: Exception) { + return Result.failure(e) + } + } + + /** + * Menambahkan data harian ke deteksi rutin yang sedang aktif + */ + suspend fun addDailyRoutineDetection( + routineDocId: String, + dayNumber: Int, + emotion: String, + activity: String, + gadAnswers: List, + totalScore: Int, + routineSessionManager: RoutineSessionManager // Add this parameter + ): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + // Create the daily data + val dailyData = DailyDetectionData( + emosi = emotion, + kegiatan = activity, + gad1 = gadAnswers.getOrElse(0) { 0 }, + gad2 = gadAnswers.getOrElse(1) { 0 }, + gad3 = gadAnswers.getOrElse(2) { 0 }, + gad4 = gadAnswers.getOrElse(3) { 0 }, + gad5 = gadAnswers.getOrElse(4) { 0 }, + gad6 = gadAnswers.getOrElse(5) { 0 }, + gad7 = gadAnswers.getOrElse(6) { 0 }, + tanggal = Timestamp(Date()), + totalSkor = totalScore, + severity = getSeverityLevel(totalScore) + ) + + // Update Firestore document + val result = firebaseService.addDailyDataToRoutineDetection( + userId, + routineDocId, + dayNumber, + dailyData + ) + + // If successful, mark this day as completed in the local session manager + if (result.isSuccess) { + routineSessionManager.saveFormCompletionForToday() + Log.d("AnxietyRepository", "Successfully saved routine data for day $dayNumber") + + // Verify the save was actually successful by checking the data + val verifyResult = firebaseService.getRoutineDetections(userId) + if (verifyResult.isSuccess) { + val routineData = verifyResult.getOrNull() + ?.find { it.first == routineDocId }?.second + + if (routineData != null) { + val hasDayData = routineData.deteksiHarian.containsKey(dayNumber.toString()) + Log.d("AnxietyRepository", "Verification - data for day $dayNumber exists: $hasDayData") + } + } + } + + return result + } catch (e: Exception) { + Log.e("AnxietyRepository", "Error adding daily routine data", e) + return Result.failure(e) + } + } + + /** + * Membuat sesi deteksi rutin baru sekaligus dengan data hari pertama + */ + suspend fun createRoutineDetectionWithFirstDayData( + sessionType: String, + emotion: String, + activity: String, + gadAnswers: List, + totalScore: Int + ): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + // Setup waktu dan tanggal + val startDate = Calendar.getInstance() + val endDate = Calendar.getInstance() + val currentDate = Date() + val dayOfWeek = getDayOfWeek(Calendar.getInstance()) + + // Menentukan tanggal berakhir berdasarkan tipe sesi + when(sessionType) { + "1_WEEK" -> endDate.add(Calendar.DATE, 7) + "2_WEEKS" -> endDate.add(Calendar.DATE, 14) + "1_MONTH" -> endDate.add(Calendar.DATE, 30) + else -> endDate.add(Calendar.DATE, 7) // Default 1 minggu + } + + // Menentukan tingkat keparahan + val severity = getSeverityLevel(totalScore) + + // Data untuk hari pertama + val dailyData = DailyDetectionData( + emosi = emotion, + kegiatan = activity, + gad1 = gadAnswers.getOrElse(0) { 0 }, + gad2 = gadAnswers.getOrElse(1) { 0 }, + gad3 = gadAnswers.getOrElse(2) { 0 }, + gad4 = gadAnswers.getOrElse(3) { 0 }, + gad5 = gadAnswers.getOrElse(4) { 0 }, + gad6 = gadAnswers.getOrElse(5) { 0 }, + gad7 = gadAnswers.getOrElse(6) { 0 }, + tanggal = Timestamp(currentDate), + totalSkor = totalScore, + severity = severity + ) + + // Map untuk data harian dengan hari 1 + val dailyDataMap = mapOf("1" to dailyData) + + // Membuat model deteksi rutin dengan data hari pertama + val routineDetection = RoutineDetectionModel( + aktif = true, + deteksiHarian = dailyDataMap, + hariSkorRendah = dayOfWeek, + hariSkorTinggi = dayOfWeek, + skorRendah = totalScore, + skorTinggi = totalScore, + periode = sessionType, + tanggalMulai = Timestamp(startDate.time), + tanggalSelesai = Timestamp(endDate.time) + ) + + Log.d("AnxietyRepository", "Creating routine detection with first day data") + return firebaseService.addRoutineDetection(userId, routineDetection) + } catch (e: Exception) { + Log.e("AnxietyRepository", "Error creating routine with first day data", e) + return Result.failure(e) + } + } + + /** + * Mendapatkan deteksi rutin yang aktif saat ini + */ + suspend fun getActiveRoutineDetection(): Result?> { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + return firebaseService.getActiveRoutineDetection(userId) + } + + /** + * Mengakhiri sesi deteksi rutin yang aktif + */ + suspend fun endRoutineDetection(routineDocId: String): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + return firebaseService.updateRoutineDetectionStatus(userId, routineDocId, false) + } + + /** + * Memeriksa apakah deteksi rutin masih valid berdasarkan tanggal berakhir + */ + suspend fun isRoutineDetectionStillValid(routineDocId: String): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + val routineResult = firebaseService.getRoutineDetections(userId) + if (routineResult.isFailure) return Result.failure(routineResult.exceptionOrNull()!!) + + val routineList = routineResult.getOrNull() + val routineData = routineList?.find { it.first == routineDocId }?.second + ?: return Result.success(false) + + if (!routineData.aktif) return Result.success(false) + + val endDate = routineData.tanggalSelesai.toDate() + val currentDate = Date() + + return Result.success(currentDate.before(endDate)) + } catch (e: Exception) { + return Result.failure(e) + } + } + +// /** +// * Mendapatkan hari saat ini dalam sesi deteksi rutin (dimulai dari 1) +// */ +// suspend fun getCurrentDayInRoutineSession(routineDocId: String): Result { +// val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) +// +// try { +// val routineResult = firebaseService.getRoutineDetections(userId) +// if (routineResult.isFailure) return Result.failure(routineResult.exceptionOrNull()!!) +// +// val routineList = routineResult.getOrNull() +// val routineData = routineList?.find { it.first == routineDocId }?.second +// ?: return Result.failure(Exception("Deteksi rutin tidak ditemukan")) +// +// val startDate = routineData.tanggalMulai.toDate() +// val currentDate = Date() +// +// // Menghitung selisih hari +// val diffInMillis = currentDate.time - startDate.time +// val diffInDays = TimeUnit.MILLISECONDS.toDays(diffInMillis).toInt() +// +// // Hari dalam sesi dimulai dari 1 +// return Result.success(diffInDays + 1) +// } catch (e: Exception) { +// return Result.failure(e) +// } +// } + + /** + * Mendapatkan durasi total sesi deteksi rutin dalam hari + */ + suspend fun getRoutineSessionDuration(routineDocId: String): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + val routineResult = firebaseService.getRoutineDetections(userId) + if (routineResult.isFailure) return Result.failure(routineResult.exceptionOrNull()!!) + + val routineList = routineResult.getOrNull() + val routineData = routineList?.find { it.first == routineDocId }?.second + ?: return Result.failure(Exception("Deteksi rutin tidak ditemukan")) + + val startDate = routineData.tanggalMulai.toDate() + val endDate = routineData.tanggalSelesai.toDate() + + // Menghitung selisih hari + val diffInMillis = endDate.time - startDate.time + val diffInDays = TimeUnit.MILLISECONDS.toDays(diffInMillis).toInt() + + return Result.success(diffInDays + 1) + } catch (e: Exception) { + return Result.failure(e) + } + } + + /** + * Memeriksa apakah pengguna sudah melakukan deteksi hari ini dalam sesi rutin + */ + suspend fun hasCompletedRoutineDetectionToday(routineDocId: String): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + + try { + val routineResult = firebaseService.getRoutineDetections(userId) + if (routineResult.isFailure) return Result.failure(routineResult.exceptionOrNull()!!) + + val routineList = routineResult.getOrNull() + val routineData = routineList?.find { it.first == routineDocId }?.second + ?: return Result.failure(Exception("Deteksi rutin tidak ditemukan")) + + // Mendapatkan hari saat ini dalam sesi + val currentDayResult = getCurrentDayInRoutineSession(routineDocId) + if (currentDayResult.isFailure) return Result.failure(currentDayResult.exceptionOrNull()!!) + + val currentDay = currentDayResult.getOrNull()!! + + // Cek apakah ada data untuk hari ini + val dailyData = routineData.deteksiHarian[currentDay.toString()] + + return Result.success(dailyData != null) + } catch (e: Exception) { + return Result.failure(e) + } + } + + // Fungsi helper untuk mendapatkan hari dalam seminggu + private fun getDayOfWeek(calendar: Calendar): String { + return when (calendar.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> "Senin" + Calendar.TUESDAY -> "Selasa" + Calendar.WEDNESDAY -> "Rabu" + Calendar.THURSDAY -> "Kamis" + Calendar.FRIDAY -> "Jumat" + Calendar.SATURDAY -> "Sabtu" + Calendar.SUNDAY -> "Minggu" + else -> "" + } + } + + // Fungsi helper untuk menentukan tingkat keparahan + private fun getSeverityLevel(totalScore: Int): String { + return when { + totalScore in 0..4 -> "Minimal" + totalScore in 5..9 -> "Ringan" + totalScore in 10..14 -> "Sedang" + totalScore >= 15 -> "Parah" + else -> "Tidak diketahui" + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/repository/AuthRepository.kt b/app/src/main/java/com/example/app_skripsi/data/repository/AuthRepository.kt new file mode 100644 index 0000000..48568fe --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/repository/AuthRepository.kt @@ -0,0 +1,36 @@ +package com.example.app_skripsi.data.repository + +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.model.UserModel + +class AuthRepository(private val firebaseService: FirebaseService) { + + suspend fun registerUser(email: String, password: String, user: UserModel): Result { + return firebaseService.registerUser(email, password, user) + } + + suspend fun loginUser(email: String, password: String): Result { + return firebaseService.loginUser(email, password) + } + + suspend fun sendPasswordResetEmail(email: String): Result { + return firebaseService.sendPasswordResetEmail(email) + } + + suspend fun logoutUser() { + firebaseService.logoutUser() + } + + + suspend fun getCurrentUserId(): String? { + return firebaseService.getCurrentUserId() + } + // 🔹 Ambil data user dari Firestore + suspend fun getUserData(userId: String): Result { + return firebaseService.getDocumentById( + collection = "users_students/data/userId", + documentId = userId, + clazz = UserModel::class.java + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/repository/DiaryRepository.kt b/app/src/main/java/com/example/app_skripsi/data/repository/DiaryRepository.kt new file mode 100644 index 0000000..dbee49d --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/repository/DiaryRepository.kt @@ -0,0 +1,19 @@ +package com.example.app_skripsi.data.repository + +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.model.DiaryModel + +class DiaryRepository(private val firebaseService: FirebaseService) { + + suspend fun addDiary(diary: DiaryModel): Result { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + return firebaseService.addDiary(userId, diary) + } + + + suspend fun getDiaries(): Result> { + val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in")) + return firebaseService.getDiaries(userId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/repository/UserRepository.kt b/app/src/main/java/com/example/app_skripsi/data/repository/UserRepository.kt new file mode 100644 index 0000000..a4c1702 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/repository/UserRepository.kt @@ -0,0 +1,77 @@ +package com.example.app_skripsi.data.repository + +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.user.UserDao +import com.example.app_skripsi.data.local.user.UserEntity +import com.example.app_skripsi.data.model.UserModel +import kotlinx.coroutines.flow.first + +class UserRepository( + private val firebaseService: FirebaseService, + private val userDao: UserDao +) { + + // ✅ Check SQLite First, then Fallback to Firebase + suspend fun getUser(userId: String): UserEntity? { + val localUser = userDao.getUserById(userId) // ✅ Fetch from SQLite + if (localUser != null) { + android.util.Log.d("UserData", "📦 Retrieved from SQLite: $userId") + return localUser + } + + // 🔥 Fetch from Firebase if not available + val firebaseUser = firebaseService.getUserData(userId) + return if (firebaseUser.isSuccess) { + firebaseUser.getOrNull()?.let { user -> + val userEntity = UserEntity( + userId = userId, + nama = user.nama, + email = user.email, + jenisKelamin = user.jenisKelamin, + umur = user.umur + ) + userDao.insertUser(userEntity) // ✅ Cache in SQLite + android.util.Log.d("UserData", "🔥 Retrieved from Firebase and Cached: $userId") + userEntity + } + } else { + android.util.Log.e("UserData", "❌ Failed to get user from Firebase: $userId") + null + } + } + + // ✅ Get from SQLite directly + suspend fun getUserFromLocal(userId: String): UserEntity? { + return userDao.getUserById(userId) + } + + // ✅ Get from Firebase directly + suspend fun getUserFromFirebase(userId: String): Result { + return firebaseService.getDocumentById( + collection = "users_students/data/userId", + documentId = userId, + clazz = UserModel::class.java + ) + } + + // ✅ Save to SQLite + suspend fun insertUser(user: UserEntity) { + userDao.insertUser(user) + } + + // ✅ Sync latest user data from SQLite to Firebase before logout + suspend fun updateUserToFirebase(user: UserEntity): Result { + return firebaseService.addDocument( + collection = "users_students/data/userId", + documentId = user.userId, + data = user + ) + } + + // ✅ Clear SQLite when user logs out + suspend fun clearLocalDatabase() { + android.util.Log.e("SQLite", "🗑 Deleting all local user data...") + userDao.deleteAllUsers() + android.util.Log.e("SQLite", "✅ All local user data deleted") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/data/repository/VideoRepository.kt b/app/src/main/java/com/example/app_skripsi/data/repository/VideoRepository.kt new file mode 100644 index 0000000..6b89161 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/data/repository/VideoRepository.kt @@ -0,0 +1,17 @@ +package com.example.app_skripsi.data.repository + +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.model.VideoModel + +class VideoRepository(private val firebaseService: FirebaseService) { + + suspend fun getAllVideos(): Result> { + return firebaseService.getVideos() + } + + // Tetap sediakan metode ini untuk kompatibilitas jika diperlukan + suspend fun getVideosByCategory(category: String): Result> { + // Abaikan kategori, ambil semua video + return firebaseService.getVideos() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/receiver/BootCompletedReceiver.kt b/app/src/main/java/com/example/app_skripsi/receiver/BootCompletedReceiver.kt new file mode 100644 index 0000000..1a7425a --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/receiver/BootCompletedReceiver.kt @@ -0,0 +1,46 @@ +package com.example.app_skripsi.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.utils.NotificationSchedulerManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class BootCompletedReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "BootCompletedReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + Log.d(TAG, "Device boot completed, checking notification settings") + CoroutineScope(Dispatchers.IO).launch { + // Check if prediction notifications were enabled + val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + val notificationsEnabled = prefs.getBoolean(NotificationSchedulerManager.PREF_NOTIFICATIONS_ENABLED, false) + + if (notificationsEnabled) { + Log.d(TAG, "Rescheduling prediction notifications after boot") + val manager = NotificationSchedulerManager(context) + manager.scheduleDailyPredictionNotificationWithAlarm(8, 0) + } + // Check routine reminders + val routineSessionManager = RoutineSessionManager(context) + val userId = routineSessionManager.getUserId() + + if (userId != null && routineSessionManager.isSessionStillActive()) { + Log.d(TAG, "Rescheduling routine reminders for user: $userId") + val manager = NotificationSchedulerManager(context) + manager.scheduleRoutineFormRemindersWithAlarm(userId) + } + } + + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/receiver/BootReceiver.kt b/app/src/main/java/com/example/app_skripsi/receiver/BootReceiver.kt new file mode 100644 index 0000000..a86543e --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/receiver/BootReceiver.kt @@ -0,0 +1,60 @@ +package com.example.app_skripsi.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.utils.NotificationSchedulerManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * Broadcast receiver that listens for device boot completed events + * and reschedules necessary notifications if there is an active routine session. + */ +class BootReceiver : BroadcastReceiver() { + private lateinit var sessionManager: SessionManager + private lateinit var notificationManager: NotificationSchedulerManager + + companion object { + private const val TAG = "BootReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED || + intent.action == "android.intent.action.QUICKBOOT_POWERON") { + + Log.d(TAG, "Device boot completed, checking for active sessions") + + // Use coroutine to handle async operations + CoroutineScope(Dispatchers.IO).launch { + try { + // Check if there's an active routine session + val routineSessionManager = RoutineSessionManager(context) + val notificationManager = NotificationSchedulerManager(context) + val userId = sessionManager.sessionUserId.first() + + val isSessionActive = routineSessionManager.isSessionStillActive() + + if (isSessionActive) { + Log.d(TAG, "Active routine session found after boot, rescheduling reminders") + notificationManager.scheduleRoutineFormRemindersWithAlarm(userId!!) + } else { + Log.d(TAG, "No active routine session found after boot") + } + + // Check if prediction notifications should be rescheduled + // (You might want to store this preference in SharedPreferences) + // For now, we'll skip this unless the user has explicitly enabled it + + } catch (e: Exception) { + Log.e(TAG, "Error rescheduling notifications after boot", e) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/receiver/PredictionAlarmReceiver.kt b/app/src/main/java/com/example/app_skripsi/receiver/PredictionAlarmReceiver.kt new file mode 100644 index 0000000..b4888a9 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/receiver/PredictionAlarmReceiver.kt @@ -0,0 +1,34 @@ +package com.example.app_skripsi.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.example.app_skripsi.utils.NotificationSchedulerManager +import com.example.app_skripsi.workers.PredictionNotificationWorker +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class PredictionAlarmReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "PredictionAlarmReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "Alarm received at: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())}") + + // Execute prediction notification worker + val workRequest = OneTimeWorkRequestBuilder() + .build() + + WorkManager.getInstance(context).enqueue(workRequest) + + // Schedule next alarm for tomorrow + val schedulerManager = NotificationSchedulerManager(context) + schedulerManager.scheduleDailyPredictionNotificationWithAlarm(8, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/receiver/RoutineFormAlarmReceiver.kt b/app/src/main/java/com/example/app_skripsi/receiver/RoutineFormAlarmReceiver.kt new file mode 100644 index 0000000..3a674d8 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/receiver/RoutineFormAlarmReceiver.kt @@ -0,0 +1,63 @@ +package com.example.app_skripsi.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.workDataOf +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.utils.NotificationSchedulerManager +import com.example.app_skripsi.workers.RoutineFormReminderWorker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class RoutineFormAlarmReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "RoutineFormAlarmReceiver" + const val EXTRA_REMINDER_NUMBER = "reminderNumber" + const val EXTRA_USER_ID = "userId" + } + + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "Alarm received at: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())}") + + val reminderNumber = intent.getIntExtra(EXTRA_REMINDER_NUMBER, 1) + val userId = intent.getStringExtra(EXTRA_USER_ID) + + if (userId.isNullOrEmpty()) { + Log.e(TAG, "User ID is null or empty") + return + } + + // Cek dulu apakah masih ada sesi aktif dan form belum diisi + CoroutineScope(Dispatchers.IO).launch { + val routineSessionManager = RoutineSessionManager(context) + + if (routineSessionManager.isSessionStillActive() && !routineSessionManager.hasCompletedFormToday()) { + // Execute worker + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf( + RoutineFormReminderWorker.KEY_REMINDER_NUMBER to reminderNumber, + "userId" to userId + )) + .build() + + WorkManager.getInstance(context).enqueue(workRequest) + } else { + Log.d(TAG, "Session not active or form already completed today, skipping notification") + } + + // Jadwalkan ulang alarm berikutnya untuk besok + val schedulerManager = NotificationSchedulerManager(context) + schedulerManager.scheduleNextRoutineReminder(reminderNumber, userId) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/AuthViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/AuthViewModel.kt new file mode 100644 index 0000000..e26fcc9 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/AuthViewModel.kt @@ -0,0 +1,24 @@ +package com.example.app_skripsi.ui.auth + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.app_skripsi.data.firebase.AuthService +import com.google.firebase.auth.FirebaseUser + +class AuthViewModel(private val authService: AuthService = AuthService()):ViewModel() { + private val _user = MutableLiveData() + val user: LiveData get() = _user + + private val _loading = MutableLiveData() + val loading: LiveData get() = _loading + + fun checkCurrentUser() { + _user.value = authService.getCurrentUser() + } + + fun logout() { + authService.logoutUser() + _user.value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/AuthViewModelFactory.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/AuthViewModelFactory.kt new file mode 100644 index 0000000..3ba6577 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/AuthViewModelFactory.kt @@ -0,0 +1,25 @@ +package com.example.app_skripsi.ui.auth + + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.data.repository.AuthRepository +import com.example.app_skripsi.ui.auth.forgotPw.ForgotPasswordViewModel +import com.example.app_skripsi.ui.auth.login.LoginViewModel +import com.example.app_skripsi.ui.auth.register.RegisterViewModel + +@Suppress("UNCHECKED_CAST") +class AuthViewModelFactory(private val authRepository: AuthRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return when { + + modelClass.isAssignableFrom(com.example.app_skripsi.ui.auth.register.RegisterViewModel::class.java) -> { + RegisterViewModel(authRepository) as T + } + modelClass.isAssignableFrom(ForgotPasswordViewModel::class.java) -> { + ForgotPasswordViewModel(authRepository) as T + } + else -> throw IllegalArgumentException("Unknown ViewModel class") + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/forgotPw/ForgotPasswordViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/forgotPw/ForgotPasswordViewModel.kt new file mode 100644 index 0000000..fdd8901 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/forgotPw/ForgotPasswordViewModel.kt @@ -0,0 +1,25 @@ +package com.example.app_skripsi.ui.auth.forgotPw + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.repository.AuthRepository +import kotlinx.coroutines.launch + +class ForgotPasswordViewModel(private val authRepository: AuthRepository) : ViewModel() { + private val _resetResult = MutableLiveData>() + val resetResult: LiveData> get() = _resetResult + + private val _loading = MutableLiveData() + val loading: LiveData get() = _loading + + fun sendPasswordReset(email: String) { + _loading.value = true + viewModelScope.launch { + val result = authRepository.sendPasswordResetEmail(email) + _resetResult.value = result + _loading.value = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/forgotPw/ForgotPwActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/forgotPw/ForgotPwActivity.kt new file mode 100644 index 0000000..f593d91 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/forgotPw/ForgotPwActivity.kt @@ -0,0 +1,91 @@ +package com.example.app_skripsi.ui.auth.forgotPw + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.Observer +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.repository.AuthRepository +import com.example.app_skripsi.databinding.ActivityForgotPwBinding +import com.example.app_skripsi.ui.auth.AuthViewModelFactory +import com.example.app_skripsi.utils.ToastUtils + +class ForgotPwActivity : AppCompatActivity() { + private var _binding : ActivityForgotPwBinding? = null + private val binding get() = _binding!! + + private val viewModel: ForgotPasswordViewModel by viewModels { + AuthViewModelFactory(AuthRepository(FirebaseService())) + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityForgotPwBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding() + v.setPadding(systemBars.left + v.paddingLeft, systemBars.top , + systemBars.right + v.paddingRight, systemBars.bottom ) + insets + } + + binding.resetPasswordButton.setOnClickListener { + val email = binding.emailReqField.text.toString().trim() + if (email.isEmpty()) { + ToastUtils.showToast(this, "Email harus diisi", position = ToastUtils.Position.TOP) + } else { + viewModel.sendPasswordReset(email) + } + } + + observeViewModel() + setupTextWatchers() + } + + private fun observeViewModel() { + viewModel.resetResult.observe(this, Observer { result -> + if (result.isSuccess) { + ToastUtils.showToast( + this, + "Link reset password telah dikirim ke email Anda.", + position = ToastUtils.Position.TOP + ) + finish() + } else { + ToastUtils.showToast( + this, + "Gagal mengirim link: ${result.exceptionOrNull()?.message}", + position = ToastUtils.Position.TOP + ) + } + }) + + viewModel.loading.observe(this, Observer { isLoading -> + binding.resetPasswordButton.isEnabled = !isLoading + }) + } + + private fun setButtonState() { + val isAllFieldFilled = binding.emailReqField.text.toString().isNotEmpty() + + binding.resetPasswordButton.isEnabled = isAllFieldFilled + } + + private fun setupTextWatchers() { + binding.emailReqField.addTextChangedListener { setButtonState() } + + } + + override fun onDestroy() { + super.onDestroy() + _binding = null // Menghapus binding untuk menghindari memory leak + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginActivity.kt new file mode 100644 index 0000000..97ec6cf --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginActivity.kt @@ -0,0 +1,235 @@ +package com.example.app_skripsi.ui.auth.login + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.MainActivity +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.AppDatabase +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.data.local.user.UserDao +import com.example.app_skripsi.data.local.user.UserEntity +import com.example.app_skripsi.data.repository.AuthRepository +import com.example.app_skripsi.data.repository.UserRepository +import com.example.app_skripsi.databinding.ActivityLoginBinding +import com.example.app_skripsi.ui.auth.AuthViewModelFactory +import com.example.app_skripsi.ui.auth.forgotPw.ForgotPwActivity +import com.example.app_skripsi.ui.auth.register.RegisterActivity +import com.example.app_skripsi.ui.dashboard.DashboardActivity +import com.example.app_skripsi.utils.ToastUtils +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit + +class LoginActivity : AppCompatActivity() { + private var _binding : ActivityLoginBinding? = null + private val binding get() = _binding!! + + private val viewModel: LoginViewModel by viewModels { + val database = AppDatabase.getDatabase(application) // Get Room database instance + val userDao = database.userDao() // Get UserDao from database + + LoginViewModelFactory( + application, + AuthRepository(FirebaseService()), + UserRepository(FirebaseService(), userDao) + ) + } + + + private var email:String? = null + + private var backPresssedTime : Long= 0 + + private var btnState : Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + +// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> +// val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) +// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) +// insets +// } + + binding.signupLink.setOnClickListener { + val intent = Intent(this, RegisterActivity::class.java) + startActivity(intent) + overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left) + } + + binding.loginBtn.setOnClickListener { + email = binding.emailField.text.toString().trim() + val password = binding.passwordField.text.toString().trim() + + if (email.isNullOrEmpty() || password.isEmpty()) { + ToastUtils.showToast(this, "Email dan Password harus diisi", position = ToastUtils.Position.TOP) + + } else { + viewModel.login(email!!, password) + } + } + + binding.forgotPasswordLink.setOnClickListener { + val intent = Intent( this, ForgotPwActivity::class.java) + startActivity(intent) + overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left) + + } + + binding.tlPasswordField.setEndIconOnClickListener { + val isVisible = binding.passwordField.inputType != android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + binding.passwordField.inputType = if (isVisible) { + android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + } else { + android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + } + binding.passwordField.setSelection(binding.passwordField.text?.length ?: 0) + + // Ubah ikon saat state berubah + binding.tlPasswordField.endIconDrawable = if (isVisible) { + ContextCompat.getDrawable(this, R.drawable.hide) + } else { + ContextCompat.getDrawable(this, R.drawable.show) + } + } + + observeViewModel() + setupTextWatchers() + setButtonState() + } + + private fun observeViewModel() { + viewModel.loginResult.observe(this, Observer { result -> + if (result.isSuccess) { + val user = result.getOrNull() + if (user != null) { + ToastUtils.showToast(this, "Login berhasil!", position = ToastUtils.Position.BOTTOM) + + saveSessionAndNavigate(user) +// val intent = Intent(this, DashboardActivity::class.java).apply { +// putExtra("USER_NAME", user.nama) +// putExtra("USER_EMAIL", user.email) +// putExtra("USER_GENDER", user.jenisKelamin) +// putExtra("USER_AGE", user.umur) +// flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK +// } +// startActivity(intent) +// overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left) +// finish() + } + } else { + ToastUtils.showToast(this, "Login gagal: ${result.exceptionOrNull()?.message}", position = ToastUtils.Position.TOP) + Log.e(TAG,"Login gagal: ${result.exceptionOrNull()?.message}") + } + }) + } + + private fun saveSessionAndNavigate(user: UserEntity) { + // Simpan session userId dan token + val userId = user.userId + val token = "dummy_token" // Ganti dengan token yang didapat dari Firebase + val expiresAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24) + + lifecycleScope.launch { + try { + // Simpan session di SessionManager + val sessionManager = SessionManager(application) + sessionManager.saveSession(token, userId, expiresAt) + + // Pulihkan sesi deteksi rutin jika ada + val routineSessionManager = RoutineSessionManager(this@LoginActivity) + val firebaseService = FirebaseService() + + // Simpan user ID di RoutineSessionManager + routineSessionManager.setUserId(userId) + + // Coba pulihkan sesi dari Firebase + val restored = routineSessionManager.restoreSessionFromFirebase(firebaseService) + + if (restored) { + Log.d(TAG, "Berhasil memulihkan sesi deteksi rutin untuk user: $userId") + } else { + Log.d(TAG, "Tidak ada sesi deteksi rutin aktif untuk dipulihkan") + } + + // Pindah ke DashboardActivity dengan membawa data userId + val intent = Intent(this@LoginActivity, DashboardActivity::class.java).apply { + putExtra("USER_ID", userId) + } + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + finish() + } catch (e: Exception) { + Log.e(TAG, "Error saat memulihkan sesi deteksi rutin: ${e.message}") + + // Tetap navigasi ke Dashboard meskipun restore sesi gagal + val intent = Intent(this@LoginActivity, DashboardActivity::class.java).apply { + putExtra("USER_ID", userId) + } + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + finish() + } + } + } + + // ✅ Helper function untuk validasi field + private fun isAllFieldsFilled(): Boolean { + return binding.emailField.text.toString().isNotEmpty() && + binding.passwordField.text.toString().isNotEmpty() + } + + private fun setButtonState() { + btnState = isAllFieldsFilled() + binding.loginBtn.isEnabled = btnState + + if (btnState) { + binding.loginBtn.setBackgroundColor( + ContextCompat.getColor(this,R.color.bluePrimary) + ) + } else { + binding.loginBtn.setBackgroundColor( + ContextCompat.getColor(this, R.color.gray400) + ) + } + } + + private fun setupTextWatchers() { + binding.emailField.addTextChangedListener { setButtonState() } + binding.passwordField.addTextChangedListener { setButtonState() } + } + + // Handle double back press untuk keluar aplikasi + override fun onBackPressed() { + val currentTime = System.currentTimeMillis() + if (currentTime - backPresssedTime <= 2000) { + super.onBackPressed() // Keluar aplikasi + } else { + backPresssedTime = currentTime + Toast.makeText(this, "Tekan sekali lagi untuk keluar", Toast.LENGTH_SHORT).show() + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null // Menghapus binding untuk menghindari memory leak + } + + companion object { + private const val TAG = "LoginActivity" + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginViewModel.kt new file mode 100644 index 0000000..e829391 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginViewModel.kt @@ -0,0 +1,136 @@ +package com.example.app_skripsi.ui.auth.login + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.firebase.AuthService +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.data.local.user.UserEntity +import com.example.app_skripsi.data.model.UserModel +import com.example.app_skripsi.data.repository.AuthRepository +import com.example.app_skripsi.data.repository.UserRepository +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit + +class LoginViewModel( + application: Application, + private val authRepository: AuthRepository, + private val userRepository: UserRepository +) : ViewModel() { + + private val sessionManager = SessionManager(application) + + private val _loginResult = MutableLiveData>() + val loginResult: LiveData> get() = _loginResult + + private val _loading = MutableLiveData() + val loading: LiveData get() = _loading + + fun login(email: String, password: String) { + _loading.value = true + viewModelScope.launch { + val authResult = authRepository.loginUser(email, password) + if (authResult.isSuccess) { + val userId = authRepository.getCurrentUserId() + if (userId != null) { + // ✅ Fetch user data from SQLite first, then Firebase if not available + val userEntity = userRepository.getUser(userId) + if (userEntity != null) { + _loginResult.value = Result.success(userEntity) + + // 🔹 Generate session token from Firebase + val firebaseUser = FirebaseAuth.getInstance().currentUser + firebaseUser?.getIdToken(true)?.addOnSuccessListener { result -> + val token = result.token ?: "no_token" + val expiresAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24) + + viewModelScope.launch { + sessionManager.saveSession(token, userId, expiresAt) + android.util.Log.d("SessionManager", "Token Created: $token, Expiry: $expiresAt") + } + } + } else { + _loginResult.value = Result.failure(Exception("Failed to fetch user data")) + } + } else { + _loginResult.value = Result.failure(Exception("Failed to get userId")) + } + } else { + _loginResult.value = Result.failure(authResult.exceptionOrNull()!!) + } + _loading.value = false + } + } + +// fun login(email: String, password: String) { +// _loading.value = true +// viewModelScope.launch { +// val loginResult = authRepository.loginUser(email, password) +// if (loginResult.isSuccess) { +// val userId = authRepository.getCurrentUserId() +// if (userId != null) { +// val localUser = userRepository.getUserFromLocal(userId) +// _loginResult.value = Result.success(localUser) +//// if (localUser != null) { +//// android.util.Log.d("LoginFlow", "✅ Using SQLite Data for user: $userId") +//// } else { +//// val userData = userRepository.getUserFromFirebase(userId) +//// if (userData.isSuccess) { +//// val user = userData.getOrNull() +//// if (user != null) { +//// val userEntity = UserEntity( +//// userId = userId, +//// nama = user.nama, +//// email = user.email, +//// jenisKelamin = user.jenisKelamin, +//// umur = user.umur +//// ) +//// userRepository.insertUser(userEntity) // ✅ Save to SQLite +//// _loginResult.value = Result.success(userEntity) +//// android.util.Log.d("LoginFlow", "🔥 Using Firebase Data and Cached: $userId") +//// } +//// } else { +//// _loginResult.value = Result.failure(Exception("Failed to retrieve user data")) +//// android.util.Log.e("LoginFlow", "❌ Failed to get data for user: $userId") +//// } +//// } +// +// // ✅ Store token session +// // Ambil token asli dari Firebase +// val firebaseUser = FirebaseAuth.getInstance().currentUser +// firebaseUser?.getIdToken(true) +// ?.addOnSuccessListener { result -> +// val token = result.token ?: "no_token" +// val expiresAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24) +// +// // ✅ Jalankan di coroutine agar tidak error +// viewModelScope.launch { +// sessionManager.saveSession(token, userId, expiresAt) +// android.util.Log.d("SessionManager", "Token Created: $token, Expiry: $expiresAt") +// } +// } +// } else { +// _loginResult.value = Result.failure(Exception("Failed to get userId")) +// android.util.Log.d("LoginFlow", "Failed to get userId") +// } +// } else { +// _loginResult.value = Result.failure(loginResult.exceptionOrNull()!!) +// } +// _loading.value = false +// } +// } +} + + +// Simpan session setelah login berhasil +// val token = "dummy_token_${System.currentTimeMillis()}" // Bisa diganti dengan token asli +// val expiresAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24) // Expired dalam 24 jam +// sessionManager.saveSession(token, userId, expiresAt) +// +// // 🔹 Cetak Log Token +// android.util.Log.d("SessionManager", "Token Created: $token, Expiry: $expiresAt") \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginViewModelFactory.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginViewModelFactory.kt new file mode 100644 index 0000000..bdd3004 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/login/LoginViewModelFactory.kt @@ -0,0 +1,24 @@ +package com.example.app_skripsi.ui.auth.login + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.data.repository.AuthRepository +import com.example.app_skripsi.data.repository.UserRepository + +@Suppress("UNCHECKED_CAST") +class LoginViewModelFactory( + private val application: Application, + private val authRepository: AuthRepository, + private val userRepository: UserRepository +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return when { + modelClass.isAssignableFrom(LoginViewModel::class.java) -> { + LoginViewModel(application, authRepository, userRepository) as T + } + else -> throw IllegalArgumentException("Unknown ViewModel class") + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/register/RegisterActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/register/RegisterActivity.kt new file mode 100644 index 0000000..c245fe3 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/register/RegisterActivity.kt @@ -0,0 +1,227 @@ +package com.example.app_skripsi.ui.auth.register + +import android.app.AlertDialog +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.Observer +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.model.UserModel +import com.example.app_skripsi.data.repository.AuthRepository +import com.example.app_skripsi.databinding.ActivityRegisterBinding +import com.example.app_skripsi.ui.auth.AuthViewModelFactory +import com.example.app_skripsi.ui.auth.login.LoginActivity +import com.example.app_skripsi.utils.ToastUtils + +class RegisterActivity : AppCompatActivity() { + private var _binding : ActivityRegisterBinding? = null + private val binding get() =_binding!! + + private val viewModel: RegisterViewModel by viewModels { + AuthViewModelFactory(AuthRepository(FirebaseService())) + } + private var btnState : Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding =ActivityRegisterBinding.inflate(layoutInflater) + setContentView(binding.root) + + onBackPressedDispatcher.addCallback(this) { + showBackConfirmationDialog() + overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + } + +// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> +// val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) +// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) +// insets +// } + // Navigasi kembali ke LoginActivity + binding.signInLink.setOnClickListener { + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + finish() + } + binding.registerBtn.setOnClickListener { + val email = binding.emailField.text.toString().trim() + val password = binding.passwordField.text.toString().trim() + val confirmPassword = binding.confirmPasswordField.text.toString().trim() + val name = binding.nameField.text.toString().trim() + val gender = binding.actvGender.text.toString().trim() + val age = binding.ageField.text.toString().trim().toIntOrNull() ?: 0 + + if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || + name.isEmpty() || gender.isEmpty() || age == 0) { + ToastUtils.showToast(this, "Semua field harus diisi", position = ToastUtils.Position.TOP) + return@setOnClickListener + } + + // 🔹 Pastikan password valid + if (!isValidPassword(password)) { + ToastUtils.showToast(this, "Password harus minimal 8 karakter, ada huruf kapital, angka, dan simbol.", position = ToastUtils.Position.TOP) + return@setOnClickListener + } + + // 🔹 Pastikan password dan konfirmasi password cocok + if (password != confirmPassword) { + ToastUtils.showToast(this, "Konfirmasi password tidak cocok", position = ToastUtils.Position.TOP) + return@setOnClickListener + } + + val user = UserModel( + nama = name, + email = email, + jenisKelamin = gender, + umur = age + ) + viewModel.register(email, password, user) + } + + + + val gender = resources.getStringArray(R.array.gender_array) + val genderAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, gender) + binding!!.actvGender.setAdapter(genderAdapter) + + binding.tlPassword.setEndIconOnClickListener { + val isVisible = binding.passwordField.inputType != android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + binding.passwordField.inputType = if (isVisible) { + android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + } else { + android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + } + binding.passwordField.setSelection(binding.passwordField.text?.length ?: 0) + + // Ubah ikon saat state berubah + binding.tlPassword.endIconDrawable = if (isVisible) { + ContextCompat.getDrawable(this, R.drawable.hide) + } else { + ContextCompat.getDrawable(this, R.drawable.show) + } + } + + binding.tlConfirmPassword.setEndIconOnClickListener { + val isVisible = binding.confirmPasswordField.inputType != android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + binding.confirmPasswordField.inputType = if (isVisible) { + android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + } else { + android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD + } + binding.confirmPasswordField.setSelection(binding.confirmPasswordField.text?.length ?: 0) + + // Ubah ikon saat state berubah + binding.tlConfirmPassword.endIconDrawable = if (isVisible) { + ContextCompat.getDrawable(this, R.drawable.hide) + } else { + ContextCompat.getDrawable(this, R.drawable.show) + } + } + + observeViewModel() + setupTextWatchers() + setButtonState() + + } + + private fun observeViewModel() { + viewModel.registerResult.observe(this, Observer { result -> + if (result.isSuccess) { + ToastUtils.showToast(this, "Registrasi berhasil, silakan login.", position = ToastUtils.Position.TOP) + startActivity(Intent(this, LoginActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + }) + finish() + overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + } else { + ToastUtils.showToast(this,"Registrasi gagal: ${result.exceptionOrNull()?.message}", position = ToastUtils.Position.TOP) + } + }) + + viewModel.loading.observe(this, Observer { isLoading -> + binding.registerBtn.isEnabled = !isLoading + }) + } + + private fun isAllFieldsFilled(): Boolean { + return binding.emailField.text.toString().isNotEmpty() && + binding.passwordField.text.toString().isNotEmpty() && + binding.confirmPasswordField.text.toString().isNotEmpty() && binding.nameField.text.toString().isNotEmpty() && + binding.ageField.text.toString().isNotEmpty() && binding.actvGender.text.toString().isNotEmpty() + } + + private fun setButtonState() { + btnState = isAllFieldsFilled() + binding.registerBtn.isEnabled = btnState + + if (btnState) { + binding.registerBtn.setBackgroundColor( + ContextCompat.getColor(this,R.color.bluePrimary) + ) + } else { + binding.registerBtn.setBackgroundColor( + ContextCompat.getColor(this, R.color.gray400) + ) + } + } + + + private fun setupTextWatchers() { + binding.emailField.addTextChangedListener { setButtonState() } + binding.passwordField.addTextChangedListener { setButtonState() } + binding.confirmPasswordField.addTextChangedListener { setButtonState() } + } + + + + // Fungsi untuk menampilkan dialog konfirmasi saat kembali + private fun showBackConfirmationDialog() { + AlertDialog.Builder(this).apply { + setTitle("Konfirmasi") + setMessage("Apakah Anda yakin ingin kembali? Data yang sudah diisi akan hilang.") + setPositiveButton("Ya") { _: DialogInterface, _: Int -> + val intent = Intent(this@RegisterActivity, LoginActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + finish() + } + setNegativeButton("Tidak") { dialog: DialogInterface, _: Int -> + dialog.dismiss() + } + create().show() + } + } + + /** 🔹 Validasi Password dengan aturan yang ketat */ + private fun isValidPassword(password: String): Boolean { + val passwordPattern = "^(?=.*[A-Z])(?=.*\\d)(?=.*[@#\$%^&+=!]).{8,}$" + return password.matches(passwordPattern.toRegex()) + } + +// override fun onBackPressed() { +// +// showBackConfirmationDialog() +// +// } + + + + override fun onDestroy() { + super.onDestroy() + _binding = null // Menghapus binding untuk menghindari memory leak + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/auth/register/RegisterViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/auth/register/RegisterViewModel.kt new file mode 100644 index 0000000..588c66f --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/auth/register/RegisterViewModel.kt @@ -0,0 +1,28 @@ +package com.example.app_skripsi.ui.auth.register + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.firebase.AuthService +import com.example.app_skripsi.data.model.UserModel +import com.example.app_skripsi.data.repository.AuthRepository +import com.google.firebase.auth.FirebaseUser +import kotlinx.coroutines.launch + +class RegisterViewModel(private val authRepository: AuthRepository) : ViewModel() { + private val _registerResult = MutableLiveData>() + val registerResult: LiveData> get() = _registerResult + + private val _loading = MutableLiveData() + val loading: LiveData get() = _loading + + fun register(email: String, password: String, user: UserModel) { + _loading.value = true + viewModelScope.launch { + val result = authRepository.registerUser(email, password, user) + _registerResult.value = result + _loading.value = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/DailyDetectionAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/DailyDetectionAdapter.kt new file mode 100644 index 0000000..458fa32 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/DailyDetectionAdapter.kt @@ -0,0 +1,97 @@ +package com.example.app_skripsi.ui.checkanxiety.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.DailyDetectionData +import java.text.SimpleDateFormat +import java.util.Locale + +class DailyDetectionAdapter( + private val dailyDetections: List> +) : RecyclerView.Adapter() { + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val tvTanggal: TextView = view.findViewById(R.id.tv_tanggal) + val ivSeeDetail: ImageView = view.findViewById(R.id.iv_see_detail) + val llHarian: LinearLayout = view.findViewById(R.id.llHarian) + val tvEmotion: TextView = view.findViewById(R.id.tvEmotion) + val tvActivity: TextView = view.findViewById(R.id.tvActivity) + val layoutGADScores: LinearLayout = view.findViewById(R.id.layoutGADScores) + val tvTotalSkor: TextView = view.findViewById(R.id.tvTotalSkor) // Tambahkan referensi ke TextView total skor + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_deteksi_harian, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val (dayNumber, dailyData) = dailyDetections[position] + + // Format tanggal + val dateFormat = SimpleDateFormat("EEEE, dd-MM-yyyy", Locale("id", "ID")) + val formattedDate = dateFormat.format(dailyData.tanggal.toDate()) + + // Set tanggal + holder.tvTanggal.text = "Hari -${dayNumber} | $formattedDate" + + // Setup toggle untuk melihat detail + holder.ivSeeDetail.setOnClickListener { + if (holder.llHarian.visibility == View.VISIBLE) { + holder.llHarian.visibility = View.GONE + holder.ivSeeDetail.setImageResource(R.drawable.add) // Ganti dengan icon plus + } else { + holder.llHarian.visibility = View.VISIBLE + holder.ivSeeDetail.setImageResource(R.drawable.add_2) // Ganti dengan icon minus + } + } + + // Set emosi dan aktivitas + holder.tvEmotion.text = "Emosi: ${dailyData.emosi}" + holder.tvActivity.text = "Kegiatan: ${dailyData.kegiatan}" + + // Set total skor + holder.tvTotalSkor.text = "Total Skor GAD 7: ${dailyData.totalSkor}" + + // Hapus semua view GAD lama + holder.layoutGADScores.removeAllViews() + + // Tambahkan GAD scores + addGADScoreTextView(holder, "GAD-1: ${getGADScoreText(dailyData.gad1)}", dailyData.gad1) + addGADScoreTextView(holder, "GAD-2: ${getGADScoreText(dailyData.gad2)}", dailyData.gad2) + addGADScoreTextView(holder, "GAD-3: ${getGADScoreText(dailyData.gad3)}", dailyData.gad3) + addGADScoreTextView(holder, "GAD-4: ${getGADScoreText(dailyData.gad4)}", dailyData.gad4) + addGADScoreTextView(holder, "GAD-5: ${getGADScoreText(dailyData.gad5)}", dailyData.gad5) + addGADScoreTextView(holder, "GAD-6: ${getGADScoreText(dailyData.gad6)}", dailyData.gad6) + addGADScoreTextView(holder, "GAD-7: ${getGADScoreText(dailyData.gad7)}", dailyData.gad7) + } + + private fun addGADScoreTextView(holder: ViewHolder, scoreText: String, scoreValue: Int) { + val textView = TextView(holder.itemView.context).apply { + text = scoreText + setPadding(0, 4, 0, 0) + textSize = 14f // Sesuaikan dengan ukuran yang diinginkan + setTypeface(resources.getFont(R.font.inter_light)) // Sesuaikan dengan font yang ada + } + holder.layoutGADScores.addView(textView) + } + + private fun getGADScoreText(score: Int): String { + return when (score) { + 0 -> "Tidak sama sekali" + 1 -> "Beberapa hari" + 2 -> "Lebih dari setengah hari" + 3 -> "Hampir setiap hari" + else -> "Tidak diketahui" + } + } + + override fun getItemCount() = dailyDetections.size +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyActivity.kt new file mode 100644 index 0000000..0b17b7c --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyActivity.kt @@ -0,0 +1,550 @@ +package com.example.app_skripsi.ui.checkanxiety + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.RadioGroup +import android.widget.Toast +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.R +import com.example.app_skripsi.databinding.ActivityFormAnxietyBinding +import androidx.viewpager2.widget.ViewPager2 +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.repository.AnxietyRepository +import com.example.app_skripsi.utils.NotificationSchedulerManager +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class FormAnxietyActivity : AppCompatActivity() { + private var _binding: ActivityFormAnxietyBinding? = null + private val binding get() = _binding!! + + private lateinit var formSessionManager: FormSessionManager + private lateinit var routineSessionManager: RoutineSessionManager + var detectionType: String = "QUICK" // Default ke deteksi singkat + + lateinit var viewModel: FormAnxietyViewModel + private lateinit var firebaseService: FirebaseService + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityFormAnxietyBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Inisialisasi formSessionManager dan routineSessionManager + formSessionManager = FormSessionManager(this) + routineSessionManager = RoutineSessionManager(this) + firebaseService = FirebaseService() + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val bottomNav = insets.getInsets(WindowInsetsCompat.Type.ime()) + v.setPadding(systemBars.left + v.paddingLeft, systemBars.top + v.paddingTop, + systemBars.right + v.paddingRight, bottomNav.bottom + v.paddingBottom) + insets + } + + // Hide status bar & navigation bar + hideSystemUI() + + // Inisialisasi repository dan managers + val firebaseService = FirebaseService() + val anxietyRepository = AnxietyRepository(firebaseService) + formSessionManager = FormSessionManager(this) + routineSessionManager = RoutineSessionManager(this) + + // Inisialisasi ViewModel dengan factory + val factory = FormAnxietyViewModelFactory(anxietyRepository, formSessionManager, routineSessionManager) + viewModel = ViewModelProvider(this, factory)[FormAnxietyViewModel::class.java] + + +// Ambil tipe deteksi dari intent + detectionType = intent.getStringExtra("DETECTION_TYPE") ?: "QUICK" + Log.d(TAG, "Detection type from Intent: $detectionType") + + // Jika deteksi rutin, cek dulu apakah sudah mengisi hari ini + // Di dalam onCreate di FormAnxietyActivity, bagian deteksi rutin + // Di FormAnxietyActivity.onCreate + if (detectionType == "ROUTINE") { + lifecycleScope.launch { + try { + val isSessionActive = routineSessionManager.isSessionStillActive() + Log.d(TAG, "Routine detection requested, session active: $isSessionActive") + + if (!isSessionActive) { + // Jika belum ada sesi aktif, tampilkan dialog pemilihan + Log.d(TAG, "No active routine session, showing dialog") + showSessionSelectionDialog() + return@launch + } + + // Ada sesi aktif, cek apakah sudah mengisi hari ini + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + Log.d(TAG, "Active session found, completed today: $hasCompletedToday") + + if (hasCompletedToday) { + AlertDialog.Builder(this@FormAnxietyActivity) + .setTitle("Deteksi Sudah Dilakukan") + .setMessage("Anda sudah mengisi form deteksi kecemasan untuk hari ini. Silakan lakukan pengisian lagi besok.") + .setPositiveButton("Kembali") { _, _ -> finish() } + .setCancelable(false) + .show() + return@launch + } + + // Lanjutkan dengan sesi yang aktif + updateSessionInfoInToolbar() + setupFormSession() + } catch (e: Exception) { + Log.e(TAG, "Error checking routine status", e) + // Tampilkan dialog pemilihan jika terjadi error + showSessionSelectionDialog() + } + } + } else { + // Setup untuk deteksi singkat + setupFormSession() + } + + // Observe viewmodel state + observeViewModelState() + + // Back button handling + onBackPressedDispatcher.addCallback(this) { + showBackConfirmationDialog() + overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right) + } + } + + // Metode terpisah untuk setup form session agar kode lebih rapi + private fun setupFormSession() { + lifecycleScope.launch { + // Reset session dan mulai baru untuk memastikan konsistensi + formSessionManager.resetSession() + formSessionManager.startSession() + formSessionManager.saveDetectionType(detectionType) + Log.d(TAG, "Form session reset and started with type: $detectionType") + + // Setup ViewPager2 + setupViewPager() + + // Mulai dari halaman pertama + binding.fragmentContainer.currentItem = 0 + updateProgressIndicator(1) + } + } + + private fun setupViewPager() { + // Menyiapkan ViewPager2 dan adapter + val adapter = FormAnxietyAdapter(this) // Adapter untuk mengelola fragment + binding.fragmentContainer.adapter = adapter // Menetapkan adapter ke ViewPager2 + binding.fragmentContainer.isUserInputEnabled = false + + // Menambahkan callback untuk mengupdate progress indicator + binding.fragmentContainer.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + updateProgressIndicator(position + 1) // Update indikator berdasarkan posisi + } + }) + } + + private fun handleDetectionTypeFlow() { + // Cek jenis deteksi + if (detectionType == "QUICK") { + lifecycleScope.launch { + // Step 1: Reset dan mulai dari awal untuk deteksi cepat + formSessionManager.resetSession() + formSessionManager.startSession() + formSessionManager.saveDetectionType("QUICK") + + // Step 2: Persiapkan UI + setupViewPager() + + // Step 3: Mulai dari fragment pertama + binding.fragmentContainer.currentItem = 0 + + // Step 4: Update indikator + updateProgressIndicator(1) + + // Step 5: Log untuk debugging + Log.d(TAG, "handleDetectionTypeFlow: Memulai form QUICK") + } + } else { + // Deteksi rutin + lifecycleScope.launch { + val isSessionActive = routineSessionManager.isSessionStillActive() + + if (!isSessionActive) { + // Belum ada sesi aktif, tampilkan dialog pemilihan durasi + Log.d(TAG, "handleDetectionTypeFlow: Tidak ada sesi rutin aktif, tampilkan dialog pemilihan durasi") + showSessionSelectionDialog() + } else { + // Sesi sudah aktif, cek apakah sudah mengisi hari ini + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + if (hasCompletedToday) { + Toast.makeText( + this@FormAnxietyActivity, + "Anda sudah mengisi form deteksi kecemasan hari ini", + Toast.LENGTH_LONG + ).show() + Log.d(TAG, "handleDetectionTypeFlow: Sudah mengisi form hari ini, keluar") + finish() + return@launch + } + + // Update judul untuk menampilkan info sesi + updateSessionInfoInToolbar() + + // Step 1: Reset dan mulai dari awal + formSessionManager.resetSession() + formSessionManager.startSession() + formSessionManager.saveDetectionType("ROUTINE") + + // Step 2: Persiapkan UI + setupViewPager() + + // Step 3: Mulai dari fragment pertama + binding.fragmentContainer.currentItem = 0 + + // Step 4: Update indikator + updateProgressIndicator(1) + + // Step 5: Log untuk debugging + Log.d(TAG, "handleDetectionTypeFlow: Memulai form ROUTINE untuk sesi aktif") + } + } + } + } + + private fun observeViewModelState() { + // Observe loading state + viewModel.isLoading.observe(this) { isLoading -> + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + } + + // Observe error messages + viewModel.errorMessage.observe(this) { message -> + if (message.isNotEmpty()) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + } + + // Observe routine session info updates + viewModel.routineSessionInfo.observe(this) { info -> + // Update toolbar with session info + updateToolbarWithSessionInfo(info) + } + + // Observe navigation to result + viewModel.navigateToResult.observe(this) { shouldNavigate -> + if (shouldNavigate) { + // Navigate to result screen + val intent = Intent(this, HasilAnxietyShortActivity::class.java) + startActivity(intent) + viewModel.resetNavigationState() + } + } + } + + private fun updateToolbarWithSessionInfo(info: Triple) { + val sessionType = when(info.first) { + "1_WEEK" -> "1 Minggu" + "2_WEEKS" -> "2 Minggu" + "1_MONTH" -> "1 Bulan" + else -> info.first + } + + supportActionBar?.title = "Deteksi Rutin" + supportActionBar?.subtitle = "Sesi $sessionType - Hari ${info.second} dari ${info.third}" + } + + private fun showRoutineConflictDialog() { + AlertDialog.Builder(this) + .setTitle("Deteksi Rutin Aktif") + .setMessage("Anda saat ini memilih form deteksi singkat, dan Anda sudah terdaftar deteksi rutin. Form deteksi singkat ini akan masuk kedalam penjadwalan rutin harian deteksi rutin. Apakah Anda yakin?") + .setPositiveButton("Ya, Lanjutkan") { _, _ -> + // Ubah tipe deteksi menjadi rutin agar masuk ke penjadwalan + detectionType = "ROUTINE" + lifecycleScope.launch { + formSessionManager.resetSession() + formSessionManager.startSession() + formSessionManager.saveDetectionType("ROUTINE") + Log.d(TAG, "Changed detection type to ROUTINE after conflict dialog") + + // Update UI jika diperlukan + updateSessionInfoInToolbar() + + // Mulai dari fragment pertama + binding.fragmentContainer.currentItem = 0 + updateProgressIndicator(1) + } + } + .setNegativeButton("Tidak, Kembali") { _, _ -> + // Reset form dan kembali + lifecycleScope.launch { + formSessionManager.resetSession() + finish() + } + } + .setCancelable(false) + .show() + } + + private fun checkSessionStatusAndNavigate() { + lifecycleScope.launch { + // Step 1: Reset session dan buat baru untuk memastikan bersih + formSessionManager.resetSession() + formSessionManager.startSession() + + // Step 2: Refresh tipe deteksi untuk memastikan konsistensi + formSessionManager.saveDetectionType(detectionType) + + // Step 3: Selalu mulai dari halaman pertama + binding.fragmentContainer.currentItem = 0 + + // Step 4: Update indikator progres + updateProgressIndicator(1) + + // Step 5: Log untuk debugging + Log.d(TAG, "checkSessionStatusAndNavigate: Memulai form baru dengan tipe $detectionType") + } + } + + private fun resumeFromLastStep() { + lifecycleScope.launch { + val currentStep = formSessionManager.currentStep.first() + Log.d(TAG, "Resuming from step: $currentStep") + + when { + currentStep == "permission" -> binding.fragmentContainer.currentItem = 0 + currentStep == "emotion" -> binding.fragmentContainer.currentItem = 1 + currentStep == "activity" -> binding.fragmentContainer.currentItem = 2 + currentStep.startsWith("gad_") -> { + // Ekstrak nomor pertanyaan GAD dari string "gad_X" + val questionNumberString = currentStep.substringAfter("gad_") + if (questionNumberString == "completed") { + // Hanya navigasi ke hasil jika semua skor GAD ada + var allAnswersExist = true + for (i in 0..6) { + val answer = formSessionManager.getGadAnswer(i) + if (answer < 0) { + allAnswersExist = false + break + } + } + + if (allAnswersExist) { + val intent = Intent(this@FormAnxietyActivity, HasilAnxietyShortActivity::class.java) + startActivity(intent) + finish() + } else { + // Jika tidak semua jawaban ada, mulai dari awal + binding.fragmentContainer.currentItem = 0 + } + } else { + try { + val questionNumber = questionNumberString.toInt() + // Pertanyaan GAD dimulai dari indeks 3 di ViewPager + binding.fragmentContainer.currentItem = 2 + questionNumber + } catch (e: NumberFormatException) { + // Default ke pertanyaan GAD pertama jika error + binding.fragmentContainer.currentItem = 3 + } + } + } + else -> binding.fragmentContainer.currentItem = 0 + } + + // Update progress indicator + updateProgressIndicator(binding.fragmentContainer.currentItem + 1) + } + } + + private fun showSessionSelectionDialog() { + val dialogView = layoutInflater.inflate(R.layout.dialog_session_selection, null) + val radioGroup = dialogView.findViewById(R.id.rgSessionType) + + Log.d(TAG, "Showing session selection dialog") + + // Pastikan ada opsi terpilih default + radioGroup.check(R.id.rbOneWeek) + + val dialog = AlertDialog.Builder(this) + .setTitle("Pilih Durasi Sesi") + .setView(dialogView) + .setCancelable(false) + .setPositiveButton("Mulai Sesi") { _, _ -> + val selectedSessionType = when(radioGroup.checkedRadioButtonId) { + R.id.rbOneWeek -> "1_WEEK" + R.id.rbTwoWeeks -> "2_WEEKS" + R.id.rbOneMonth -> "1_MONTH" + else -> "1_WEEK" // Default ke 1 minggu + } + + Log.d(TAG, "Selected session type: $selectedSessionType") + + lifecycleScope.launch { + try { + // Ambil user ID saat ini + val userId = firebaseService.getCurrentUserId() + if (userId.isNullOrEmpty()) { + Toast.makeText(this@FormAnxietyActivity, + "Anda belum login", + Toast.LENGTH_SHORT).show() + finish() + return@launch + } + + // Simpan ke local storage dengan user ID + routineSessionManager.startNewSession(selectedSessionType, userId) + Log.d(TAG, "Started local session of type: $selectedSessionType for user: $userId") + + // Schedule reminders dengan AlarmManager + val notificationManager = NotificationSchedulerManager(this@FormAnxietyActivity) + notificationManager.scheduleRoutineFormRemindersWithAlarm(userId) + + // Lanjutkan setup form + formSessionManager.resetSession() + formSessionManager.startSession() + formSessionManager.saveDetectionType("ROUTINE") + + // Mulai dari fragment pertama + setupViewPager() + binding.fragmentContainer.currentItem = 0 + updateProgressIndicator(1) + } catch (e: Exception) { + Log.e(TAG, "Error starting routine session", e) + Toast.makeText(this@FormAnxietyActivity, + "Gagal memulai sesi rutin: ${e.message}", + Toast.LENGTH_LONG).show() + } + } + } + .setNegativeButton("Batal") { _, _ -> + // Kembali ke deteksi singkat jika batal + detectionType = "QUICK" + lifecycleScope.launch { + formSessionManager.saveDetectionType("QUICK") + } + Toast.makeText(this, "Mode deteksi diubah ke Deteksi Singkat", Toast.LENGTH_SHORT).show() + finish() + } + .create() + + dialog.show() + } + + private fun updateSessionInfoInToolbar() { + lifecycleScope.launch { + val sessionType = routineSessionManager.getSessionTypeDisplay() + val currentDay = routineSessionManager.getCurrentSessionDay() + val totalDays = routineSessionManager.getSessionDurationInDays() + + supportActionBar?.title = "Deteksi Rutin" + supportActionBar?.subtitle = "Sesi $sessionType - Hari $currentDay dari $totalDays" + } + } + + // Fungsi untuk memperbarui progress indicator + fun updateProgressIndicator(currentPage: Int) { + val progressIndicator = binding.llProgressIndicator + for (i in 0 until progressIndicator.childCount) { + val view = progressIndicator.getChildAt(i) + view.setBackgroundResource( + if (i < currentPage) R.drawable.round_indicator_active + else R.drawable.round_indicator_inactive + ) + } + } + + private fun showBackConfirmationDialog() { + // Menampilkan dialog konfirmasi apakah yakin ingin keluar + AlertDialog.Builder(this) + .setMessage("Apakah Anda yakin ingin keluar? Semua data yang belum disimpan akan hilang.") + .setPositiveButton("Ya") { _, _ -> + // Menghapus sesi di DataStore jika keluar + lifecycleScope.launch { + formSessionManager.resetSession() + } + finish() // Menutup Activity + } + .setNegativeButton("Tidak", null) + .show() + } + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, window.decorView).apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + // Fungsi untuk navigasi langsung ke halaman GAD tertentu + // Fungsi untuk navigasi langsung ke halaman GAD tertentu + fun navigateToGadQuestion(questionNumber: Int) { + try { + // Index pertanyaan GAD di ViewPager (GAD dimulai dari index 3) + val pageIndex = 3 + questionNumber + + Log.d(TAG, "navigateToGadQuestion: Pindah ke pertanyaan $questionNumber (position $pageIndex)") + + // Gunakan runOnUiThread untuk memastikan perubahan UI aman + runOnUiThread { + if (binding.fragmentContainer.currentItem != pageIndex) { + binding.fragmentContainer.currentItem = pageIndex + + // Update progress indicator + updateProgressIndicator(pageIndex + 1) + } + } + } catch (e: Exception) { + Log.e(TAG, "Error saat navigasi ke pertanyaan GAD", e) + } + } + + // Fungsi untuk check apakah routine session aktif (dapat dipanggil dari fragment) + fun isRoutineSessionActive(): Boolean { + return viewModel.activeRoutineDetection.value != null + } + + // Fungsi untuk mendapatkan tipe deteksi saat ini + fun getCurrentDetectionType(): String { + return detectionType + } + + // Metode untuk debugging DataStore - untuk debugging saja + private fun debugDataStore() { + viewModel.getDetectionTypeInfo() + } + + override fun onResume() { + super.onResume() + // Check kembali active routine session setiap kali activity di-resume + if (detectionType == "ROUTINE") { + viewModel.checkActiveRoutineSession() + } + } + + + override fun onDestroy() { + super.onDestroy() + _binding = null + } + + companion object { + private const val TAG = "FormAnxietyActivity" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyAdapter.kt new file mode 100644 index 0000000..d050664 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyAdapter.kt @@ -0,0 +1,48 @@ +package com.example.app_skripsi.ui.checkanxiety + +import android.util.Log +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.example.app_skripsi.ui.checkanxiety.formanxiety.PermissionFragment +import com.example.app_skripsi.ui.checkanxiety.formanxiety.EmotionFragment +import com.example.app_skripsi.ui.checkanxiety.formanxiety.ActivityFragment +import com.example.app_skripsi.ui.checkanxiety.formanxiety.GadQuestionFragment + +class FormAnxietyAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { + + // Mengembalikan jumlah fragment yang ada + override fun getItemCount(): Int = 10 + + // Mengembalikan fragment yang sesuai dengan posisi yang dipilih + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> PermissionFragment() // Fragment pertama untuk izin + 1 -> EmotionFragment() // Fragment kedua untuk pemilihan emosi + 2 -> ActivityFragment() // Fragment ketiga untuk pemilihan aktivitas + else -> { + // Pertanyaan GAD-7 (position 3-9) + val gadPosition = position - 3 + Log.d("FormAnxietyAdapter", "Creating GAD Question Fragment position=$position, gadPosition=$gadPosition") + val isLastQuestion = gadPosition == 6 // Pertanyaan terakhir + GadQuestionFragment.newInstance( + questionNumber = gadPosition, + questionText = getGadQuestion(gadPosition), + isLast = isLastQuestion + ) + } + } + } + private fun getGadQuestion(position: Int): String { + val questions = listOf( + "Merasa gugup, cemas, atau tegang", + "Tidak mampu menghentikan atau mengendalikan rasa khawatir", + "Terlalu mengkhawatirkan berbagai hal", + "Sulit untuk rileks", + "Sangat gelisah sehingga sulit untuk duduk diam", + "Menjadi mudah tersinggung atau mudah marah", + "Merasa takut, seolah-olah ada sesuatu yang buruk mungkin terjadi" + ) + return questions[position] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyViewModel.kt new file mode 100644 index 0000000..8abbf2f --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyViewModel.kt @@ -0,0 +1,302 @@ +package com.example.app_skripsi.ui.checkanxiety + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.model.RoutineDetectionModel +import com.example.app_skripsi.data.repository.AnxietyRepository +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class FormAnxietyViewModel( + private val anxietyRepository: AnxietyRepository, + private val formSessionManager: FormSessionManager, + private val routineSessionManager: RoutineSessionManager +) : ViewModel() { + + // LiveData untuk UI state + private val _selectedEmotion = MutableLiveData() + val selectedEmotion: LiveData get() = _selectedEmotion + + private val _selectedActivity = MutableLiveData() + val selectedActivity: LiveData get() = _selectedActivity + + private val _isLoading = MutableLiveData() + val isLoading: LiveData get() = _isLoading + + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData get() = _errorMessage + + private val _navigateToResult = MutableLiveData() + val navigateToResult: LiveData get() = _navigateToResult + + private val _activeRoutineDetection = MutableLiveData?>() + val activeRoutineDetection: LiveData?> get() = _activeRoutineDetection + + private val _routineSessionInfo = MutableLiveData>() // type, currentDay, totalDays + val routineSessionInfo: LiveData> get() = _routineSessionInfo + + // LiveData untuk menyimpan jawaban GAD untuk diteruskan ke activity hasil + private val _gadAnswersList = MutableLiveData>() + val gadAnswersList: LiveData> get() = _gadAnswersList + + // Fungsi untuk mengatur emosi yang dipilih + fun setSelectedEmotion(emotion: String) { + _selectedEmotion.value = emotion + viewModelScope.launch { + formSessionManager.saveEmotion(emotion) + } + } + + // Fungsi untuk mengatur aktivitas yang dipilih + fun setSelectedActivity(activity: String) { + _selectedActivity.value = activity + viewModelScope.launch { + formSessionManager.saveActivity(activity) + } + } + + // Memeriksa apakah ada sesi rutin yang aktif + fun checkActiveRoutineSession() { + viewModelScope.launch { + _isLoading.value = true + + try { + val result = anxietyRepository.getActiveRoutineDetection() + if (result.isSuccess) { + _activeRoutineDetection.value = result.getOrNull() + + // Update info sesi rutin jika ada + updateRoutineSessionInfo() + } + } catch (e: Exception) { + Log.e("FormAnxietyViewModel", "Error checking active routine: ${e.message}") + _errorMessage.value = "Gagal memeriksa status sesi rutin: ${e.message}" + } finally { + _isLoading.value = false + } + } + } + + // Membuat sesi deteksi rutin baru + fun createRoutineSession(sessionType: String) { + viewModelScope.launch { + _isLoading.value = true + try { + // Log untuk debugging + Log.d("FormAnxietyViewModel", "Creating routine session with type: $sessionType") + + // Inisiasi sesi lokal saja + routineSessionManager.startNewSession(sessionType) + Log.d("FormAnxietyViewModel", "Local routine session created successfully") + + // Dokumen Firestore akan dibuat nanti setelah form selesai + checkActiveRoutineSession() + } catch (e: Exception) { + Log.e("FormAnxietyViewModel", "Error creating routine session: ${e.message}", e) + _errorMessage.value = "Gagal membuat sesi rutin: ${e.message}" + } finally { + _isLoading.value = false + } + } + } + + // Update informasi sesi rutin + private fun updateRoutineSessionInfo() { + viewModelScope.launch { + val activeRoutine = _activeRoutineDetection.value ?: return@launch + + try { + val currentDayResult = anxietyRepository.getCurrentDayInRoutineSession(activeRoutine.first) + val durationResult = anxietyRepository.getRoutineSessionDuration(activeRoutine.first) + + if (currentDayResult.isSuccess && durationResult.isSuccess) { + val currentDay = currentDayResult.getOrNull() ?: 1 + val totalDays = durationResult.getOrNull() ?: 7 + val sessionType = activeRoutine.second.periode + + _routineSessionInfo.value = Triple(sessionType, currentDay, totalDays) + } + } catch (e: Exception) { + Log.e("FormAnxietyViewModel", "Error updating session info: ${e.message}") + } + } + } + + // Memeriksa apakah deteksi rutin sudah dilakukan hari ini + fun checkIfRoutineDetectionCompletedToday() { + viewModelScope.launch { + val activeRoutine = _activeRoutineDetection.value ?: return@launch + + try { + val checkResult = anxietyRepository.hasCompletedRoutineDetectionToday(activeRoutine.first) + if (checkResult.isSuccess) { + val hasCompleted = checkResult.getOrNull() ?: false + if (hasCompleted) { + routineSessionManager.saveFormCompletionForToday() + } + } + } catch (e: Exception) { + Log.e("FormAnxietyViewModel", "Error checking detection completion: ${e.message}") + } + } + } + + fun saveAnxietyDetection(gadAnswers: List, totalScore: Int) { + viewModelScope.launch { + _isLoading.value = true + try { + val emotion = formSessionManager.emotion.first() + val activity = formSessionManager.activity.first() + val detectionType = formSessionManager.getDetectionType() + + // Log untuk debugging + Log.d("FormAnxietyViewModel", "Saving ${detectionType} detection - Emotion: $emotion, Activity: $activity, Score: $totalScore") + Log.d("FormAnxietyViewModel", "GAD Answers: $gadAnswers") + + // Simpan jawaban dan total skor ke DataStore untuk backup + gadAnswers.forEachIndexed { index, answer -> + formSessionManager.saveGadAnswer(index, answer) + } + formSessionManager.saveGadTotalScore(totalScore) + + // Tidak perlu menyimpan data di sini, cukup tandai selesai untuk hari ini jika rutin + if (detectionType == "ROUTINE") { + routineSessionManager.saveFormCompletionForToday() + } else { + // Untuk deteksi singkat, tetap simpan data + anxietyRepository.addShortDetection(emotion, activity, gadAnswers, totalScore) + } + + // Navigasi ke hasil + _navigateToResult.value = true + } catch (e: Exception) { + Log.e("FormAnxietyViewModel", "Error saving detection: ${e.message}") + _errorMessage.value = "Gagal menyimpan deteksi: ${e.message}" + } finally { + _isLoading.value = false + } + } + } + + // Mendapatkan deteksi rutin aktif + fun getActiveRoutineSessionInfo(): Pair>? { + val activeRoutine = _activeRoutineDetection.value ?: return null + val sessionInfo = _routineSessionInfo.value ?: return null + + val sessionType = when(sessionInfo.first) { + "1_WEEK" -> "1 Minggu" + "2_WEEKS" -> "2 Minggu" + "1_MONTH" -> "1 Bulan" + else -> sessionInfo.first + } + + return Pair(activeRoutine.first, Triple(sessionInfo.second, sessionInfo.third, sessionType)) + } + + // Validasi apakah semua jawaban GAD sudah diisi + fun validateGadAnswers(gadAnswers: List): Boolean { + // Pastikan semua pertanyaan GAD telah dijawab + return gadAnswers.size == 7 && gadAnswers.all { it != null } + } + + // Reset navigasi ke hasil + fun resetNavigationState() { + _navigateToResult.value = false + } + + // Mengecek apakah user bisa melakukan deteksi rutin hari ini + fun canDoRoutineDetectionToday(): Boolean { + var canDo = false + viewModelScope.launch { + // Cek apakah ada sesi rutin aktif + val isSessionActive = routineSessionManager.isSessionStillActive() + if (!isSessionActive) return@launch + + // Cek apakah sudah mengisi deteksi hari ini + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + canDo = !hasCompletedToday + } + return canDo + } + + // Load data dari DataStore + fun loadSavedData() { + viewModelScope.launch { + val emotion = formSessionManager.emotion.first() + if (emotion.isNotEmpty()) { + _selectedEmotion.value = emotion + } + + val activity = formSessionManager.activity.first() + if (activity.isNotEmpty()) { + _selectedActivity.value = activity + } + } + } + + // Untuk debugging + fun getDetectionTypeInfo() { + viewModelScope.launch { + val detectionType = formSessionManager.getDetectionType() + Log.d("FormAnxietyViewModel", "Current detection type: $detectionType") + + val isRoutineActive = routineSessionManager.isSessionStillActive() + Log.d("FormAnxietyViewModel", "Is routine session active: $isRoutineActive") + + if (isRoutineActive) { + val sessionType = routineSessionManager.getSessionTypeDisplay() + val currentDay = routineSessionManager.getCurrentSessionDay() + val totalDays = routineSessionManager.getSessionDurationInDays() + val lastCompletionDate = routineSessionManager.lastFormCompletionDate.first() + + Log.d("FormAnxietyViewModel", "Session type: $sessionType") + Log.d("FormAnxietyViewModel", "Current day: $currentDay of $totalDays") + Log.d("FormAnxietyViewModel", "Last completion date: $lastCompletionDate") + } + } + } + + // Di FormAnxietyViewModel, pada saat menangani navigasi ke hasil + fun navigateToResultScreen() { + viewModelScope.launch { + val emotion = formSessionManager.emotion.first() + val activity = formSessionManager.activity.first() + val totalScore = formSessionManager.gadTotalScore.first() + + // Dapatkan jawaban GAD + val gadAnswers = mutableListOf() + for (i in 0..6) { + val answer = formSessionManager.getGadAnswer(i) + if (answer > 0) { + // Konversi dari skala 1-4 ke skala 0-3 + gadAnswers.add(answer - 1) + } else { + gadAnswers.add(0) // Default 0 jika tidak ada jawaban + } + } + + // Sediakan data untuk HasilAnxietyShortActivity +// _resultData.value = HasilAnxietyData( +// totalScore = totalScore, +// emotion = emotion, +// activity = activity, +// gadAnswers = gadAnswers +// ) + + _navigateToResult.value = true + } + } + + // Data class untuk menampung data hasil + data class HasilAnxietyData( + val totalScore: Int, + val emotion: String, + val activity: String, + val gadAnswers: List + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyViewModelFactory.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyViewModelFactory.kt new file mode 100644 index 0000000..26f4f89 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/FormAnxietyViewModelFactory.kt @@ -0,0 +1,26 @@ +package com.example.app_skripsi.ui.checkanxiety + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.repository.AnxietyRepository + +class FormAnxietyViewModelFactory( + private val anxietyRepository: AnxietyRepository, + private val formSessionManager: FormSessionManager, + private val routineSessionManager: RoutineSessionManager +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(FormAnxietyViewModel::class.java)) { + return FormAnxietyViewModel( + anxietyRepository, + formSessionManager, + routineSessionManager + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/HasilAnxietyShortActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/HasilAnxietyShortActivity.kt new file mode 100644 index 0000000..2c4301d --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/HasilAnxietyShortActivity.kt @@ -0,0 +1,418 @@ +package com.example.app_skripsi.ui.checkanxiety + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.util.TypedValue +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.model.DailyDetectionData +import com.example.app_skripsi.data.model.RoutineDetectionModel +import com.example.app_skripsi.data.repository.AnxietyRepository +import com.example.app_skripsi.databinding.ActivityHasilAnxietyShortBinding +import com.example.app_skripsi.utils.NotificationSchedulerManager +import com.google.firebase.Timestamp +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean + +class HasilAnxietyShortActivity : AppCompatActivity() { + private var _binding: ActivityHasilAnxietyShortBinding? = null + private val binding get() = _binding!! + + private lateinit var formSessionManager: FormSessionManager + private lateinit var routineSessionManager: RoutineSessionManager + + // Flag untuk mencegah pemrosesan ganda + companion object { + private val isProcessing = AtomicBoolean(false) + private const val TAG = "HasilAnxietyActivity" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityHasilAnxietyShortBinding.inflate(layoutInflater) + setContentView(binding.root) + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left + v.paddingLeft, systemBars.top + v.paddingTop, + systemBars.right + v.paddingRight, systemBars.bottom + v.paddingBottom) + insets + } + + // Inisialisasi manager + formSessionManager = FormSessionManager(this) + routineSessionManager = RoutineSessionManager(this) + + // Cek apakah proses sudah berjalan untuk mencegah duplikasi + if (isProcessing.compareAndSet(false, true)) { + try { + processAnxietyResults() + } finally { + // Pastikan flag selalu direset + isProcessing.set(false) + } + } else { + Log.d(TAG, "Proses sudah berjalan, mencegah pemrosesan ganda") + } + + // Back button + binding.btnBack.setOnClickListener { + // Reset session sebelum kembali + lifecycleScope.launch { + formSessionManager.resetSession() + finish() + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + Log.d(TAG, "onNewIntent dipanggil, mencegah pemrosesan ulang") + // Tidak perlu memproses ulang, cukup tampilkan UI + } + + private fun processAnxietyResults() { + // Ambil data dari intent + val totalScore = intent.getIntExtra("TOTAL_SCORE", 0) + val emotion = intent.getStringExtra("EMOTION") ?: "Tidak Diketahui" + val activity = intent.getStringExtra("ACTIVITY") ?: "Tidak Diketahui" + val gadAnswers = intent.getIntegerArrayListExtra("GAD_ANSWERS") ?: arrayListOf() + + Log.d(TAG, "Data diterima - Score: $totalScore, Emotion: $emotion, Activity: $activity, GAD size: ${gadAnswers.size}") + + // Validasi data dan gunakan data dari session jika perlu + lifecycleScope.launch { + var finalScore = totalScore + var finalEmotion = emotion + var finalActivity = activity + var finalGadAnswers = gadAnswers + + // Jika data intent tidak valid, gunakan data dari FormSessionManager + if (totalScore == 0 || emotion == "Tidak Diketahui" || activity == "Tidak Diketahui" || gadAnswers.isEmpty()) { + Log.d(TAG, "Data intent tidak valid, menggunakan data dari FormSessionManager") + + // Menggunakan gadTotalScore.first() untuk mendapatkan nilai dari Flow + finalScore = formSessionManager.gadTotalScore.first() + finalEmotion = formSessionManager.emotion.first() + finalActivity = formSessionManager.activity.first() + + // Ambil jawaban GAD-7 dari formSessionManager + val retrievedAnswers = ArrayList() + for (i in 0..6) { + val answer = formSessionManager.getGadAnswer(i) + // Konversi ke skala 0-3 jika nilai valid + if (answer > 0) { + retrievedAnswers.add(answer - 1) + } else { + retrievedAnswers.add(0) // Default ke 0 jika tidak valid + } + } + finalGadAnswers = retrievedAnswers + + Log.d(TAG, "Data dari FormSessionManager - Score: $finalScore, Emotion: $finalEmotion, Activity: $finalActivity, GAD size: ${finalGadAnswers.size}") + } + + // Set UI dengan data yang sudah divalidasi + updateUI(finalScore, finalEmotion, finalActivity, finalGadAnswers) + + // Proses data untuk deteksi rutin jika perlu + processRoutineDetectionIfNeeded(finalScore, finalEmotion, finalActivity, finalGadAnswers) + } + } + + private suspend fun processRoutineDetectionIfNeeded( + totalScore: Int, + emotion: String, + activity: String, + gadAnswers: List + ) { + try { + val detectionType = formSessionManager.getDetectionType() + + if (detectionType == "ROUTINE") { + val firebaseService = FirebaseService() + val anxietyRepository = AnxietyRepository(firebaseService) + + // Cek apakah ini hari pertama dari sesi rutin + val currentDay = routineSessionManager.getCurrentSessionDay() + val isFirstDay = currentDay == 1 + + Log.d(TAG, "Processing routine detection for day $currentDay") + + if (isFirstDay) { + // Ambil periode sesi dari DataStore + val sessionType = routineSessionManager.sessionType.first() + Log.d(TAG, "Creating routine detection with session type: $sessionType") + + // Buat model data untuk hari ini + val calendar = Calendar.getInstance() + val currentDate = Date() + val severity = getSeverityLevel(totalScore) + val dayOfWeek = getDayOfWeek(calendar) + + // Buat DailyDetectionData + val dailyData = DailyDetectionData( + emosi = emotion, + kegiatan = activity, + gad1 = gadAnswers.getOrElse(0) { 0 }, + gad2 = gadAnswers.getOrElse(1) { 0 }, + gad3 = gadAnswers.getOrElse(2) { 0 }, + gad4 = gadAnswers.getOrElse(3) { 0 }, + gad5 = gadAnswers.getOrElse(4) { 0 }, + gad6 = gadAnswers.getOrElse(5) { 0 }, + gad7 = gadAnswers.getOrElse(6) { 0 }, + tanggal = Timestamp(currentDate), + totalSkor = totalScore, + severity = severity + ) + + // Map untuk data harian dengan hari 1 + val dailyDataMap = mapOf("1" to dailyData) + + // Membuat model rutin dengan data hari pertama + val startDate = routineSessionManager.sessionStartDate.first() + val endDate = routineSessionManager.sessionEndDate.first() + + val routineDetection = RoutineDetectionModel( + aktif = true, + deteksiHarian = dailyDataMap, + hariSkorRendah = dayOfWeek, + hariSkorTinggi = dayOfWeek, + skorRendah = totalScore, + skorTinggi = totalScore, + periode = sessionType, + tanggalMulai = Timestamp(Date(startDate)), + tanggalSelesai = Timestamp(Date(endDate)) + ) + + // Tambahkan ke Firestore + val userId = firebaseService.getCurrentUserId() + if (userId != null) { + try { + val result = firebaseService.addRoutineDetection(userId, routineDetection) + if (result.isSuccess) { + Log.d(TAG, "Successfully created routine detection document") + } else { + Log.e(TAG, "Failed to create routine detection: ${result.exceptionOrNull()?.message}") + } + } catch (e: Exception) { + Log.e(TAG, "Error creating routine detection", e) + } + } else { + Log.e(TAG, "User not logged in, cannot create routine detection") + } + } else { + // Update dokumen yang sudah ada untuk hari saat ini + val isSessionActive = routineSessionManager.isSessionStillActive() + if (isSessionActive) { + // Ambil dokumen aktif dari repository + val activeRoutineResult = anxietyRepository.getActiveRoutineDetection() + if (activeRoutineResult.isSuccess) { + val activeRoutine = activeRoutineResult.getOrNull() + if (activeRoutine != null) { + // Tambahkan data untuk hari ini ke dokumen yang sudah ada + val result = anxietyRepository.addDailyDataToExistingRoutine( + activeRoutine.first, + emotion, + activity, + gadAnswers, + totalScore, + routineSessionManager + ) + + if (result.isSuccess) { + Log.d(TAG, "Successfully added day $currentDay data to existing routine") + // Setelah berhasil menyimpan data hari ini // Cek apakah ini adalah hari terakhir dan semua data sudah lengkap + val totalDays = routineSessionManager.getSessionDurationInDays() + val currentDay = routineSessionManager.getCurrentSessionDay() + + if (currentDay >= totalDays) { + // Ini hari terakhir, akhiri sesi + val userId = firebaseService.getCurrentUserId() + val activeRoutineResult = anxietyRepository.getActiveRoutineDetection() + + if (activeRoutineResult.isSuccess && activeRoutineResult.getOrNull() != null) { + val activeRoutine = activeRoutineResult.getOrNull()!! + + // Cancel reminders setelah sesi selesai + val notificationManager = NotificationSchedulerManager(this) + notificationManager.cancelRoutineFormAlarms() + + Log.d(TAG, "Routine session completed and reminders cancelled") + + // Update status di Firestore + firebaseService.updateRoutineDetectionStatus(userId!!, activeRoutine.first, false) + + // Update local storage + routineSessionManager.endSession() + + Log.d(TAG, "Routine session completed and ended automatically") + } + } + } else { + Log.e(TAG, "Failed to add day data: ${result.exceptionOrNull()?.message}") + } + } + } + } + } + + // Tandai sebagai telah mengisi form hari ini + routineSessionManager.saveFormCompletionForToday() + } + } catch (e: Exception) { + Log.e(TAG, "Error processing routine detection", e) + } + } + + private fun updateUI( + totalScore: Int, + emotion: String, + activity: String, + gadAnswers: List + ) { + // Set current date + val currentDate = SimpleDateFormat("EEEE, d MMMM yyyy", Locale("id", "ID")).format(Date()) + binding.tvDate.text = "Hari: $currentDate" + + // Set emotion and activity + binding.tvEmotion.text = "Emosi: $emotion" + binding.tvActivity.text = "Kegiatan: $activity" + + // Set total score and anxiety level + binding.tvTotalSkor.text = "Total Skor GAD-7: $totalScore" + setAnxietyLevelAndAdvice(totalScore) + + // Set individual GAD scores + setGadScores(gadAnswers) + } + + // Helper function + private fun getSeverityLevel(totalScore: Int): String { + return when { + totalScore in 0..4 -> "Minimal" + totalScore in 5..9 -> "Ringan" + totalScore in 10..14 -> "Moderate" + totalScore >= 15 -> "Parah" + else -> "Tidak diketahui" + } + } + + private fun getDayOfWeek(calendar: Calendar): String { + return when (calendar.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> "Senin" + Calendar.TUESDAY -> "Selasa" + Calendar.WEDNESDAY -> "Rabu" + Calendar.THURSDAY -> "Kamis" + Calendar.FRIDAY -> "Jumat" + Calendar.SATURDAY -> "Sabtu" + Calendar.SUNDAY -> "Minggu" + else -> "" + } + } + + private fun setAnxietyLevelAndAdvice(totalScore: Int) { + val (level, color, advice) = when { + totalScore in 0..4 -> Triple("Minimal", R.color.thirtary, + "Tingkat kecemasan Anda minimal. Terus pertahankan kesehatan mental dan gaya hidup sehat.") + totalScore in 5..9 -> Triple("Ringan", R.color.blueSecondary, + "Anda mungkin bisa mengelola ini secara mandiri dengan teknik manajemen stres, latihan relaksasi, dan perubahan gaya hidup seperti tidur cukup dan makan makanan sehat.") + totalScore in 10..14 -> Triple("Sedang", R.color.bluePrimary, + "Tingkat kecemasan Anda moderat. Pertimbangkan untuk mencari dukungan dari teman, keluarga, atau profesional kesehatan mental.") + else -> Triple("Berat", R.color.purplePrimary, + "Tingkat kecemasan Anda tinggi. Sangat disarankan untuk segera berkonsultasi dengan profesional kesehatan mental.") + } + + binding.btnAnxietyManagement.text = "Kecemasan $level" + binding.btnAnxietyManagement.backgroundTintList = + android.content.res.ColorStateList.valueOf(ContextCompat.getColor(this,color)) + binding.tvAdvice.text = advice + } + + private fun setGadScores(gadAnswers: List) { + // Clear existing views + binding.layoutGADScores.removeAllViews() + + // Jika gadAnswers kosong, tampilkan pesan bahwa data tidak tersedia + if (gadAnswers.isEmpty()) { + val textView = TextView(this).apply { + text = "Data jawaban GAD tidak tersedia" + setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + setTextColor(ContextCompat.getColor(this@HasilAnxietyShortActivity, R.color.gray800)) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 8, 0, 8) + } + } + binding.layoutGADScores.addView(textView) + return + } + + // Pertanyaan GAD-7 + val gadQuestions = listOf( + "Merasa gugup, cemas, atau tegang", + "Tidak mampu menghentikan atau mengendalikan rasa khawatir", + "Terlalu mengkhawatirkan berbagai hal", + "Sulit untuk rileks", + "Sangat gelisah sehingga sulit untuk duduk diam", + "Menjadi mudah tersinggung atau mudah marah", + "Merasa takut, seolah-olah ada sesuatu yang buruk mungkin terjadi" + ) + + // Add GAD scores dynamically + gadQuestions.forEachIndexed { index, question -> + // Pastikan index masih dalam range gadAnswers + if (index < gadAnswers.size) { + val textView = TextView(this).apply { + text = "GAD-${index + 1}: $question - ${getGadScoreDescription(gadAnswers[index])}" + setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + setTextColor(ContextCompat.getColor(this@HasilAnxietyShortActivity, R.color.gray800)) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 8, 0, 8) + } + } + binding.layoutGADScores.addView(textView) + } + } + } + + private fun getGadScoreDescription(score: Int): String { + return when (score) { + 0 -> "Tidak Pernah" + 1 -> "Beberapa Hari" + + 2 -> "Lebih dari Setengah Hari" + 3 -> "Hampir Setiap Hari" + else -> "Tidak Diketahui" + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/RoutineResultActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/RoutineResultActivity.kt new file mode 100644 index 0000000..e76a258 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/RoutineResultActivity.kt @@ -0,0 +1,446 @@ +package com.example.app_skripsi.ui.checkanxiety + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.example.app_skripsi.R +import com.example.app_skripsi.core.knn.PersonalPredictionService +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.model.RoutineDetectionModel +import com.example.app_skripsi.databinding.ActivityRoutineResultBinding +import com.example.app_skripsi.receivers.PredictionAlarmReceiver +import com.example.app_skripsi.ui.checkanxiety.adapter.DailyDetectionAdapter +import com.example.app_skripsi.utils.NotificationSchedulerManager +import com.example.app_skripsi.workers.PredictionNotificationWorker +import com.google.android.material.button.MaterialButton +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit + +/** + * Activity for displaying routine anxiety detection results and predictions. + * This version includes functionality for scheduling prediction notifications. + */ +class RoutineResultActivity : AppCompatActivity() { + private var _binding: ActivityRoutineResultBinding? = null + private val binding get() = _binding!! + private lateinit var firebaseService: FirebaseService + private lateinit var predictionService: PersonalPredictionService + private lateinit var notificationManager: NotificationSchedulerManager + + // Flag to track if notifications are currently scheduled + private var isPredictionNotificationsActive = false + + companion object { + private const val TAG = "RoutineResultActivity" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityRoutineResultBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left + v.paddingLeft, systemBars.top+v.paddingTop, + systemBars.right +v.paddingRight, systemBars.bottom+v.paddingBottom) + insets + } + + // Initialize services + firebaseService = FirebaseService() + predictionService = PersonalPredictionService(this) + notificationManager = NotificationSchedulerManager(this) + + // Check if notifications are already scheduled + isPredictionNotificationsActive = notificationManager.isPredictionNotificationsScheduled() + + // Setup back button + binding.btnBack.setOnClickListener { + finish() + } + + // Setup notification toggle button + setupNotificationButton() + + // Load data + loadLatestCompletedRoutineDetection() + // Tambahkan tombol test di UI +// binding.btnTestNotification?.setOnClickListener { +// testImmediateNotification() +// } +// checkWorkStatus() + } + + /** + * Sets up the notification toggle button + */ + private fun setupNotificationButton() { + binding.btnToggleNotifications?.let { button -> + // Set initial button state + updateNotificationButtonState(button) + + // Set click listener + button.setOnClickListener { + if (isPredictionNotificationsActive) { + // Cancel notifications using AlarmManager + notificationManager.cancelAlarmNotifications() + isPredictionNotificationsActive = false + Toast.makeText(this, "Notifikasi prediksi dinonaktifkan", Toast.LENGTH_SHORT).show() + } else { + // Request exact alarm permission for Android 12+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (!alarmManager.canScheduleExactAlarms()) { + startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)) + Toast.makeText(this, "Izinkan pengaturan alarm untuk notifikasi", Toast.LENGTH_LONG).show() + return@setOnClickListener + } + } + + // Schedule notifications using AlarmManager + val success = notificationManager.scheduleDailyPredictionNotificationWithAlarm(8, 0) + isPredictionNotificationsActive = success + + if (success) { + Toast.makeText(this, "Notifikasi prediksi diaktifkan", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, "Gagal mengaktifkan notifikasi", Toast.LENGTH_SHORT).show() + } + } + + // Update button appearance + updateNotificationButtonState(button) + } + } + } + + /** + * Updates notification button text and appearance based on current state + */ + private fun updateNotificationButtonState(button: MaterialButton) { + if (isPredictionNotificationsActive) { + button.text = "Berhenti Notifikasi Prediksi" + button.icon = ContextCompat.getDrawable(this, R.drawable.ic_notifications_off) + button.backgroundTintList = ContextCompat.getColorStateList(this, R.color.gray500) + } else { + button.text = "Aktifkan Notifikasi Prediksi" + button.icon = ContextCompat.getDrawable(this, R.drawable.ic_notifications) + button.backgroundTintList = ContextCompat.getColorStateList(this, R.color.bluePrimary) + } + } + + // Tambahkan di RoutineResultActivity + private fun checkWorkStatus() { + WorkManager.getInstance(this) + .getWorkInfosForUniqueWork(NotificationSchedulerManager.WORK_NAME_PREDICTION) + .get() + .forEach { workInfo -> + Log.d("WorkStatus", "State: ${workInfo.state}, ID: ${workInfo.id}") + } + } + // Di RoutineResultActivity, tambah fungsi untuk test immediate notification + private fun testImmediateNotification() { + // Test immediate notification using AlarmManager + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(this, PredictionAlarmReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast( + this, + 9999, // Different request code for testing + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Set alarm to trigger in 10 seconds + val triggerTime = System.currentTimeMillis() + 10000 + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent + ) + } + + Toast.makeText(this, "Test notification scheduled in 10 seconds", Toast.LENGTH_SHORT).show() + } + + private fun loadLatestCompletedRoutineDetection() { + lifecycleScope.launch { + try { + val userId = firebaseService.getCurrentUserId() + if (userId == null) { + showError("User tidak login") + return@launch + } + + // Get all routine detections + val result = firebaseService.getRoutineDetections(userId) + if (result.isFailure) { + showError("Gagal mengambil data: ${result.exceptionOrNull()?.message}") + return@launch + } + + val routineDetections = result.getOrNull() + if (routineDetections.isNullOrEmpty()) { + showNoData() + return@launch + } + + // Filter for inactive (completed) documents + val completedDetections = routineDetections.filter { !it.second.aktif } + + if (completedDetections.isEmpty()) { + Log.d(TAG, "Tidak ada dokumen dengan status non-aktif") + showNoData() + return@launch + } + + // Sort by document ID number (deteksi_rutin_X) + val latestDetection = completedDetections.maxByOrNull { + it.first.substringAfterLast("_").toIntOrNull() ?: 0 + } + + if (latestDetection == null) { + showNoData() + return@launch + } + + // Display latest routine detection data + Log.d(TAG, "Menampilkan dokumen terbaru: ${latestDetection.first}") + displayRoutineDetection(latestDetection.first, latestDetection.second) + + } catch (e: Exception) { + Log.e(TAG, "Error loading data", e) + showError("Terjadi kesalahan: ${e.message}") + } + } + } + + private fun displayRoutineDetection(docId: String, routineDetection: RoutineDetectionModel) { + // Format period + val periode = when(routineDetection.periode) { + "1_WEEK" -> "1 Minggu" + "2_WEEKS" -> "2 Minggu" + "1_MONTH" -> "1 Bulan" + else -> routineDetection.periode + } + + // Format dates + val dateFormat = SimpleDateFormat("EEEE, dd MMMM yyyy", Locale("id", "ID")) + val tanggalMulai = dateFormat.format(routineDetection.tanggalMulai.toDate()) + val tanggalSelesai = dateFormat.format(routineDetection.tanggalSelesai.toDate()) + + // Display basic information + binding.tvPeriodic.text = "Periode Sesi: $periode" + binding.tvTglMulai.text = "Tanggal Mulai: $tanggalMulai" + binding.tvTglBerakhir.text = "Tanggal Berakhir: $tanggalSelesai" + + // Display lowest and highest scores + binding.tvSkorRendah.text = "Berdasarkan Deteksi yang Anda Isikan Secara Rutin, " + + "Skor ${routineDetection.skorRendah} adalah Skor Terendah yang Muncul di Hari ${routineDetection.hariSkorRendah}" + + binding.tvSkorTinggi.text = "Berdasarkan Deteksi yang Anda Isikan Secara Rutin, " + + "Skor ${routineDetection.skorTinggi} adalah Skor Tertinggi yang Muncul di Hari ${routineDetection.hariSkorTinggi}" + + // Display RecyclerView if there is daily data + if (routineDetection.deteksiHarian.isNotEmpty()) { + setupRecyclerView(routineDetection) + } else { + binding.recyclerViewDeteksiHarian.visibility = View.GONE + } + + // Display severity summary for historical data + displaySeveritySummary(routineDetection) + + // Generate and display predictions + displayPredictions(routineDetection) + + // Make notification button visible since we have data to create notifications + binding.btnToggleNotifications?.visibility = View.VISIBLE + } + + private fun setupRecyclerView(routineDetection: RoutineDetectionModel) { + binding.recyclerViewDeteksiHarian.layoutManager = LinearLayoutManager(this) + + // Convert Map to List> + val dailyDataList = routineDetection.deteksiHarian.entries + .map { Pair(it.key, it.value) } + .sortedBy { it.first.toIntOrNull() ?: 0 } + + val adapter = DailyDetectionAdapter(dailyDataList) + binding.recyclerViewDeteksiHarian.adapter = adapter + + // Ensure RecyclerView scrolls properly within ScrollView + binding.recyclerViewDeteksiHarian.isNestedScrollingEnabled = false + } + + private fun displaySeveritySummary(routineDetection: RoutineDetectionModel) { + // Create severity summary by day with color coding + val severitySummary = StringBuilder() + severitySummary.append("Riwayat Tingkat Kecemasan: ") + + routineDetection.deteksiHarian.entries + .sortedBy { it.key.toIntOrNull() ?: 0 } + .forEachIndexed { index, entry -> + val hari = getDayName(entry.value.tanggal.toDate()) + val severity = entry.value.severity + val colorCode = getSeverityColorHex(severity) + + if (index > 0) severitySummary.append(", ") + severitySummary.append("$hari: $severity") + } + + binding.tvShowAllDaySeverity.text = android.text.Html.fromHtml( + severitySummary.toString(), + android.text.Html.FROM_HTML_MODE_COMPACT + ) + } + + private fun displayPredictions(routineDetection: RoutineDetectionModel) { + try { + // Convert map to sorted list of DailyDetectionData + val dailyDataList = routineDetection.deteksiHarian.entries + .map { it.value } + .sortedBy { it.tanggal } + + // Get the last day number + val lastDayNumber = routineDetection.deteksiHarian.keys + .mapNotNull { it.toIntOrNull() } + .maxOrNull() ?: 0 + + // Log data points being used for prediction + Log.d(TAG, "Using ${dailyDataList.size} personal data points for prediction") + + // Generate detailed predictions using only personal data + val predictionDetails = predictionService.predictNextSevenDaysDetail( + lastDayNumber, + dailyDataList + ) + + if (predictionDetails.isEmpty()) { + binding.tvPredictedSeverity.text = "Prediksi 7 Hari: Data historis tidak cukup untuk membuat prediksi." + binding.btnToggleNotifications?.visibility = View.GONE // Hide button if no predictions + return + } + + // Format for each day with color coding by severity + val predictionHtml = StringBuilder() + predictionHtml.append("Prediksi 7 Hari ke Depan (Berdasarkan Data Personal Anda):
") + + predictionDetails.forEachIndexed { index, detail -> + // Add separator for days + if (index > 0) predictionHtml.append("
") + + // Add confidence indicator + val confidenceIndicator = when { + detail.confidence >= 0.8 -> "🟢" // High confidence + detail.confidence >= 0.5 -> "🟡" // Medium confidence + else -> "🔴" // Low confidence + } + + // Get color based on severity + val severityColor = getSeverityColorHex(detail.predictedSeverity) + + // Add day header with colored severity + predictionHtml.append("${index+1}. ${detail.dayOfWeek} $confidenceIndicator: ") + predictionHtml.append("Prediksi: ${detail.predictedSeverity} (Skor: ${detail.predictedGadScore})
") + + // Add personalized description + predictionHtml.append(detail.getReadableDescription()) + + // Add recommendation + predictionHtml.append("
Rekomendasi: ${detail.getRecommendedActivity()}") + } + + // Display predictions in HTML format + binding.tvPredictedSeverity.text = android.text.Html.fromHtml( + predictionHtml.toString(), + android.text.Html.FROM_HTML_MODE_COMPACT + ) + + // Log predictions for debugging + Log.d(TAG, "Generated ${predictionDetails.size} predictions using personal data") + predictionDetails.forEachIndexed { index, detail -> + Log.d(TAG, "$index: ${detail.dayOfWeek} - ${detail.predictedSeverity} (confidence: ${detail.confidence})") + } + } catch (e: Exception) { + Log.e(TAG, "Error displaying predictions: ${e.message}", e) + binding.tvPredictedSeverity.text = "Prediksi 7 Hari: Terjadi kesalahan dalam pembuatan prediksi." + binding.btnToggleNotifications?.visibility = View.GONE // Hide button on error + } + } + + /** + * Returns color hex code based on severity level + */ + private fun getSeverityColorHex(severity: String): String { + return when (severity) { + "Minimal" -> "#4CAF50" // Green + "Ringan" -> "#2196F3" // Blue + "Sedang" -> "#FF9800" // Orange + "Parah" -> "#F44336" // Red + else -> "#757575" // Gray + } + } + + private fun getDayName(date: Date): String { + val dayFormat = SimpleDateFormat("EEEE", Locale("id", "ID")) + return dayFormat.format(date) + } + + private fun showNoData() { + binding.tvPeriodic.text = "Tidak ada data deteksi rutin yang selesai" + binding.tvTglMulai.visibility = View.GONE + binding.tvTglBerakhir.visibility = View.GONE + binding.recyclerViewDeteksiHarian.visibility = View.GONE + + binding.tvSkorRendah.visibility = View.GONE + binding.tvSkorTinggi.visibility = View.GONE + binding.tvShowAllDaySeverity.visibility = View.GONE + binding.tvPredictedSeverity.visibility = View.GONE + binding.btnToggleNotifications?.visibility = View.GONE + } + + private fun showError(message: String) { + binding.tvPeriodic.text = "Terjadi kesalahan: $message" + binding.tvTglMulai.visibility = View.GONE + binding.tvTglBerakhir.visibility = View.GONE + binding.recyclerViewDeteksiHarian.visibility = View.GONE + + binding.tvSkorRendah.visibility = View.GONE + binding.tvSkorTinggi.visibility = View.GONE + binding.tvShowAllDaySeverity.visibility = View.GONE + binding.tvPredictedSeverity.visibility = View.GONE + binding.btnToggleNotifications?.visibility = View.GONE + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/ActivityFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/ActivityFragment.kt new file mode 100644 index 0000000..4815b4f --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/ActivityFragment.kt @@ -0,0 +1,167 @@ +package com.example.app_skripsi.ui.checkanxiety.formanxiety + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.viewpager2.widget.ViewPager2 +import com.example.app_skripsi.R +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.databinding.FragmentActivityBinding +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyActivity +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyViewModel +import com.example.app_skripsi.utils.ToastUtils +import com.google.android.material.card.MaterialCardView +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class ActivityFragment : Fragment() { + private var _binding: FragmentActivityBinding? = null + private val binding get() = _binding!! + + private lateinit var formSessionManager: FormSessionManager + private var selectedCard: MaterialCardView? = null + // Di EmotionFragment + private lateinit var viewModel: FormAnxietyViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let {} + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentActivityBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + formSessionManager = FormSessionManager(requireContext()) + // Dapatkan viewModel dari activity + viewModel = (requireActivity() as FormAnxietyActivity).viewModel + + // Setup card view selection + setupCardViewSelection() + + // Cek apakah sudah ada aktivitas yang dipilih sebelumnya + MainScope().launch { + val savedActivity = formSessionManager.activity.first() + if (savedActivity.isNotEmpty()) { + // Pilih card yang sesuai dengan aktivitas yang disimpan + when (savedActivity) { + "Belajar/Bekerja" -> binding.cardStudyWork.performClick() + "Istirahat" -> binding.cardSleep.performClick() + "Hiburan" -> binding.cardEntertainment.performClick() + "Sosialisasi" -> binding.cardSocial.performClick() + "Olahraga" -> binding.cardSport.performClick() + } + } + } + + // Di ActivityFragment, modifikasi btnLanjutkan onClick + binding.btnLanjutkan.setOnClickListener { + val selectedActivity = getSelectedActivity() + if (selectedActivity == null) { + ToastUtils.showToast( + requireContext(), + "Silakan pilih kegiatan terlebih dahulu", + duration = Toast.LENGTH_SHORT, + position = ToastUtils.Position.TOP + ) + return@setOnClickListener + } + + MainScope().launch { + formSessionManager.saveActivity(selectedActivity) + + // Lanjut ke pertanyaan GAD-7 pertama + val viewPager = activity?.findViewById(R.id.fragment_container) + viewPager?.currentItem = 3 // Index 3 adalah pertanyaan GAD-7 pertama + + // Update indikator progress + (activity as FormAnxietyActivity).updateProgressIndicator(3) + } + } + } + + private fun setupCardViewSelection() { + val cardViewsWithIcons = mapOf( + binding.cardStudyWork to Pair(binding.icWork, binding.tvWork), + binding.cardSleep to Pair(binding.icSleep, binding.tvSleep), + binding.cardEntertainment to Pair(binding.icGame, binding.tvGame), + binding.cardSocial to Pair(binding.icSocial, binding.tvSocial), + binding.cardSport to Pair(binding.icSport, binding.tvSport) + ) + + cardViewsWithIcons.forEach { (cardView, iconTextPair) -> + cardView.setOnClickListener { + // Reset semua card ke inactive state + resetAllCards(cardViewsWithIcons) + + // Set card yang dipilih ke active state + setCardActive(cardView, iconTextPair.first, iconTextPair.second) + + // Simpan card yang dipilih + selectedCard = cardView + } + } + } + + private fun resetAllCards(cardViewsWithIcons: Map>) { + cardViewsWithIcons.forEach { (cardView, iconTextPair) -> + // Reset warna card + cardView.setCardBackgroundColor(ContextCompat.getColor(requireContext(), R.color.gray200)) + + // Reset warna icon + val icon = iconTextPair.first + icon.setColorFilter(ContextCompat.getColor(requireContext(), R.color.gray200)) + icon.setBackgroundResource(R.drawable.bg_icon_activity_form_inactive) + + // Reset warna text + val text = iconTextPair.second + text.setTextColor(ContextCompat.getColor(requireContext(), R.color.bluePrimary)) + } + } + + private fun setCardActive(cardView: MaterialCardView, icon: ImageView, text: TextView) { + // Set active card color + cardView.setCardBackgroundColor(ContextCompat.getColor(requireContext(), R.color.bluePrimary)) + + // Set active icon color and background + icon.setColorFilter(ContextCompat.getColor(requireContext(), R.color.bluePrimary)) + icon.setBackgroundResource(R.drawable.bg_icon_activity_form) + + // Set active text color + text.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + } + + private fun getSelectedActivity(): String? { + return when (selectedCard) { + binding.cardStudyWork -> "Belajar/Bekerja" + binding.cardSleep -> "Istirahat" + binding.cardEntertainment -> "Hiburan" + binding.cardSocial -> "Sosialisasi" + binding.cardSport -> "Olahraga" + else -> null + } + } + + companion object { + @JvmStatic + fun newInstance() = ActivityFragment() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/EmotionFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/EmotionFragment.kt new file mode 100644 index 0000000..f512a0d --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/EmotionFragment.kt @@ -0,0 +1,146 @@ +package com.example.app_skripsi.ui.checkanxiety.formanxiety + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.viewpager2.widget.ViewPager2 +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.repository.DiaryRepository +import com.example.app_skripsi.databinding.FragmentEmotionBinding +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyActivity +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyViewModel +import com.example.app_skripsi.ui.profile.diary.DiaryViewModel +import com.example.app_skripsi.ui.profile.diary.DiaryViewModelFactory +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + + +class EmotionFragment : Fragment() { + private var _binding : FragmentEmotionBinding? = null + private val binding get() = _binding!! + + private lateinit var formSessionManager: FormSessionManager + private lateinit var emotionMap: Map + + // Ubah dari by viewModels() menjadi lateinit var + private lateinit var viewModel: FormAnxietyViewModel + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentEmotionBinding.inflate(inflater,container,false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + formSessionManager = FormSessionManager(requireContext()) + + viewModel = (requireActivity() as FormAnxietyActivity).viewModel + + // Cek apakah sudah ada emosi yang dipilih sebelumnya + MainScope().launch { + val savedEmotion = formSessionManager.emotion.first() + if (savedEmotion.isNotEmpty()) { + viewModel.setSelectedEmotion(savedEmotion) + } + } + + emotionMap = mapOf( + binding.ivHappy to "Senang", + binding.ivSad to "Sedih", + binding.ivNormal to "Normal", + binding.ivAngry to "Marah", + binding.ivFrustated to "Kecewa" + ) + + // Dalam EmotionFragment + binding.btnLanjutkan.setOnClickListener { + // Menyimpan pilihan emosi ke DataStore + val selectedEmotion = viewModel.selectedEmotion.value ?: return@setOnClickListener + MainScope().launch { + formSessionManager.saveEmotion(selectedEmotion) // Menyimpan pilihan emosi + + // Pindah ke halaman berikutnya dengan ViewPager2 + val viewPager2 = activity?.findViewById(R.id.fragment_container) + viewPager2?.currentItem = 2 // Pindah ke fragment ActivityFragment (index 2) + + // Update indikator ke langkah berikutnya + (activity as FormAnxietyActivity).updateProgressIndicator(3) // Update indikator ke langkah berikutnya + } + } + + setupEmotionSelection() + + } + + private fun setupEmotionSelection() { + // Set click listeners for all emotion images + emotionMap.keys.forEach { imageView -> + imageView.setOnClickListener { + val selectedEmotion = emotionMap[imageView] ?: return@setOnClickListener + viewModel.setSelectedEmotion(selectedEmotion) + + // Set the text visibility based on the selection + getEmotionTextView(selectedEmotion) + } + } + + // Observe the selected emotion and update UI accordingly + viewModel.selectedEmotion.observe(viewLifecycleOwner) { selectedEmotion -> + emotionMap.forEach { (imageView, emotion) -> + val color = if (emotion == selectedEmotion) R.color.bluePrimary else R.color.gray400 + imageView.setColorFilter(ContextCompat.getColor(requireContext(), color)) + // Show/hide the text based on selected emotion + if (emotion == selectedEmotion) { + // Make TextView visible for selected emotion + getEmotionTextView(emotion)?.visibility = View.VISIBLE + } else { + // Hide TextView for unselected emotions + getEmotionTextView(emotion)?.visibility = View.GONE + } + } + } + } + + // Function to get TextView based on selected emotion + private fun getEmotionTextView(emotion: String): TextView? { + return when (emotion) { + "Senang" -> binding.tvHappy + "Sedih" -> binding.tvSad + "Normal" -> binding.tvNormal + "Marah" -> binding.tvAngry + "Kecewa" -> binding.tvFrustated + else -> null + } + } + + companion object { + + @JvmStatic + fun newInstance(param1: String, param2: String) = + EmotionFragment().apply { + arguments = Bundle().apply { + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/GadQuestionFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/GadQuestionFragment.kt new file mode 100644 index 0000000..68d2229 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/GadQuestionFragment.kt @@ -0,0 +1,340 @@ +package com.example.app_skripsi.ui.checkanxiety.formanxiety + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RadioButton +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.ViewPager2 +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.repository.AnxietyRepository +import com.example.app_skripsi.databinding.FragmentGadQuestionBinding +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyActivity +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyViewModel +import com.example.app_skripsi.ui.checkanxiety.HasilAnxietyShortActivity +import com.example.app_skripsi.utils.ToastUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class GadQuestionFragment : Fragment() { + private var _binding: FragmentGadQuestionBinding? = null + private val binding get() = _binding!! + + private lateinit var viewModel: FormAnxietyViewModel + private lateinit var formSessionManager: FormSessionManager + + private var questionNumber: Int = 0 + private var questionText: String = "" // Menambahkan deklarasi variabel questionText + private var isLastQuestion: Boolean = false + private var selectedAnswer: Int = -1 + private var hasNavigated = false + + // Untuk menyimpan jawaban GAD + private var gadAnswers = mutableListOf(null, null, null, null, null, null, null) + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + questionNumber = it.getInt(ARG_QUESTION_NUMBER) + questionText = it.getString(ARG_QUESTION_TEXT, "") + isLastQuestion = it.getBoolean(ARG_IS_LAST, false) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentGadQuestionBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + formSessionManager = FormSessionManager(requireContext()) + viewModel = (activity as FormAnxietyActivity).viewModel + + Log.d(TAG, "onViewCreated: Question #$questionNumber, isLast=$isLastQuestion, text='$questionText'") + + // Inisialisasi array jawaban + gadAnswers = mutableListOf(null, null, null, null, null, null, null) + + // Setup pertanyaan + binding.tvQuestionNumber.text = "Pertanyaan ${questionNumber + 1}" + binding.tvQuestion.text = questionText // Menggunakan tvQuestion sesuai dengan binding + + // Atur teks tombol berdasarkan apakah ini pertanyaan terakhir + binding.btnLanjutkan.text = if (isLastQuestion) "Selesai" else "Selanjutnya" + + // Load jawaban yang sudah disimpan (jika ada) + loadSavedAnswer() + + // Setup radio button + setupRadioButtons() + + // Setup tombol selanjutnya + binding.btnLanjutkan.setOnClickListener { + Log.d(TAG, "Button Lanjutkan diklik untuk pertanyaan $questionNumber") + handleNextButtonClick() + } + } + + private suspend fun validateAllAnswers(): Boolean { + try { + // Log semua jawaban dari DataStore untuk debugging + Log.d(TAG, "Validasi semua jawaban GAD:") + var allValid = true + + for (i in 0..6) { + val answer = formSessionManager.getGadAnswer(i) + Log.d(TAG, "Jawaban GAD $i: $answer") + if (answer <= 0) { + allValid = false + Log.e(TAG, "Jawaban tidak valid untuk pertanyaan $i") + } + } + + return allValid + } catch (e: Exception) { + Log.e(TAG, "Error validating answers", e) + return false + } + } + + private fun loadSavedAnswer() { + lifecycleScope.launch { + try { + val savedAnswer = formSessionManager.getGadAnswer(questionNumber) + Log.d(TAG, "Memuat jawaban tersimpan untuk pertanyaan $questionNumber: $savedAnswer") + + if (savedAnswer > 0) { + // Jika ada jawaban tersimpan, set radio button + when (savedAnswer) { + 1 -> binding.rbOption1.isChecked = true + 2 -> binding.rbOption2.isChecked = true + 3 -> binding.rbOption3.isChecked = true + 4 -> binding.rbOption4.isChecked = true + } + + // Update array jawaban (konversi 1-4 menjadi 0-3) + gadAnswers[questionNumber] = savedAnswer - 1 + + // Enable tombol lanjutkan + binding.btnLanjutkan.isEnabled = true + } else { + // Tidak ada jawaban tersimpan + binding.btnLanjutkan.isEnabled = false + Log.d(TAG, "Tidak ada jawaban tersimpan untuk pertanyaan $questionNumber") + } + } catch (e: Exception) { + Log.e(TAG, "Error loading saved answer: ${e.message}") + } + } + } + + private fun setupRadioButtons() { + binding.rbOption1.setOnClickListener { updateAnswer(0) } // Not at all + binding.rbOption2.setOnClickListener { updateAnswer(1) } // Several days + binding.rbOption3.setOnClickListener { updateAnswer(2) } // More than half the days + binding.rbOption4.setOnClickListener { updateAnswer(3) } // Nearly every day + } + + private fun updateAnswer(answerValue: Int) { + // Simpan di array lokal (0-3) + gadAnswers[questionNumber] = answerValue + Log.d(TAG, "Mengupdate jawaban pertanyaan $questionNumber ke nilai $answerValue") + + // Aktifkan tombol lanjutkan + binding.btnLanjutkan.isEnabled = true + + // Simpan jawaban ke DataStore (1-4) + lifecycleScope.launch { + val valueToSave = answerValue + 1 + formSessionManager.saveGadAnswer(questionNumber, valueToSave) + Log.d(TAG, "Menyimpan ke DataStore: pertanyaan $questionNumber = $valueToSave") + } + } + + private fun handleNextButtonClick() { + // Verifikasi bahwa pertanyaan ini telah dijawab + if (gadAnswers[questionNumber] == null) { + Toast.makeText(context, "Silakan pilih salah satu jawaban", Toast.LENGTH_SHORT).show() + return + } + + lifecycleScope.launch { + // Simpan jawaban ke DataStore + val answerToSave = (gadAnswers[questionNumber] ?: 0) + 1 // Konversi 0-3 menjadi 1-4 + formSessionManager.saveGadAnswer(questionNumber, answerToSave) + Log.d(TAG, "Menyimpan jawaban untuk pertanyaan $questionNumber: $answerToSave") + + if (isLastQuestion) { + + // Double check: jika ini deteksi rutin, cek apakah sudah mengisi hari ini + val detectionType = formSessionManager.getDetectionType() + + // Periksa apakah ada session rutin yang aktif dan belum diisi hari ini + var canProceed = true + if (detectionType == "ROUTINE") { + try { + val routineSessionManager = RoutineSessionManager(requireContext()) + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + Log.d(TAG, "Checking routine completion status: hasCompletedToday=$hasCompletedToday") + + if (hasCompletedToday) { + Log.w(TAG, "Final check: User has already completed form today") + Toast.makeText( + requireContext(), + "Anda sudah mengisi form deteksi kecemasan hari ini", + Toast.LENGTH_LONG + ).show() + requireActivity().finish() + canProceed = false + } + } catch (e: Exception) { + Log.e(TAG, "Error checking form completion status", e) + // Tetap lanjutkan jika terjadi error pada pengecekan + } + } + + // Hanya lanjutkan jika belum mengisi hari ini atau bukan deteksi rutin + if (canProceed) { + // Ambil jawaban langsung dari DataStore, bukan dari array lokal + var totalScore = 0 + val allAnswers = ArrayList() + var hasAllAnswers = true + + // Log untuk debug + Log.d(TAG, "Memeriksa jawaban GAD dari DataStore:") + + for (i in 0..6) { + val answer = formSessionManager.getGadAnswer(i) + Log.d(TAG, "Jawaban GAD $i dari DataStore: $answer") + + if (answer <= 0) { // Nilai -1 berarti jawaban tidak ada + hasAllAnswers = false + Log.e(TAG, "Jawaban untuk pertanyaan $i tidak ditemukan") + break + } else { + // Simpan dengan konversi dari skala DataStore (1-4) ke skala aplikasi (0-3) + val adjustedAnswer = answer - 1 + totalScore += adjustedAnswer + allAnswers.add(adjustedAnswer) + } + } + + if (!hasAllAnswers) { + // Coba perbaiki jawaban yang hilang jika mungkin + Log.d(TAG, "Beberapa jawaban tidak lengkap, mencoba inisialisasi ulang dari pertanyaan awal") + binding.tvErrorMessage.visibility = View.VISIBLE + binding.tvErrorMessage.text = "Mohon jawab semua pertanyaan sebelumnya" + + // Tampilkan log sebagai bantuan debugging + for (i in 0..6) { + val answer = formSessionManager.getGadAnswer(i) + Log.d(TAG, "Pengecekan ulang GAD $i: $answer") + } + } else { + // Simpan total skor + formSessionManager.saveGadTotalScore(totalScore) + Log.d(TAG, "Menyimpan total skor: $totalScore") + + // Ambil data emosi dan aktivitas + val emotion = formSessionManager.getEmotion() + val activity = formSessionManager.getActivity() + + Log.d(TAG, "Data lengkap untuk hasil: Emosi=$emotion, Aktivitas=$activity, Total Skor=$totalScore") + + // Simpan data ke repository + viewModel.saveAnxietyDetection(allAnswers, totalScore) + // Tambahkan ini di akhir proses pengiriman data: + if (detectionType == "ROUTINE") { + try { + val routineSessionManager = RoutineSessionManager(requireContext()) + // Tandai pengisian hari ini sebagai BACKUP jika ViewModel gagal melakukannya + routineSessionManager.saveFormCompletionForToday() + Log.d(TAG, "GadQuestion marking routine completion for today (backup)") + } catch (e: Exception) { + Log.e(TAG, "Error saving completion status", e) + } + } + + // Navigasi ke hasil dengan flag CLEAR_TOP untuk mencegah duplikasi + val intent = Intent(requireActivity(), HasilAnxietyShortActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + intent.putExtra("TOTAL_SCORE", totalScore) + intent.putExtra("EMOTION", emotion) + intent.putExtra("ACTIVITY", activity) + intent.putIntegerArrayListExtra("GAD_ANSWERS", ArrayList(allAnswers)) + startActivity(intent) + requireActivity().finish() + + } + } + } else { + // Untuk pertanyaan non-terakhir, navigasi ke pertanyaan berikutnya + try { + // Cara 1: Gunakan fungsi navigasi di activity + val activity = requireActivity() as FormAnxietyActivity + activity.navigateToGadQuestion(questionNumber + 1) + Log.d(TAG, "Navigasi ke pertanyaan ${questionNumber + 1}") + } catch (e: Exception) { + // Cara 2: Akses ViewPager2 secara langsung jika cara 1 gagal + try { + val viewPager = requireActivity().findViewById(R.id.fragment_container) + val nextPosition = 3 + (questionNumber + 1) // position = 3 + gadQuestionNumber + viewPager?.setCurrentItem(nextPosition, true) + Log.d(TAG, "Alternatif: Navigasi langsung ke position $nextPosition") + } catch (e2: Exception) { + Log.e(TAG, "Gagal navigasi ke pertanyaan berikutnya", e2) + Toast.makeText(context, "Gagal pindah ke pertanyaan berikutnya", Toast.LENGTH_SHORT).show() + } + } + } + } + } + + private fun calculateTotalScore(): Int { + // Konversi null menjadi 0 dan jumlahkan seluruh nilai + return gadAnswers.sumOf { it ?: 0 } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + hasNavigated = false + } + + companion object { + private const val ARG_QUESTION_NUMBER = "question_number" + private const val ARG_QUESTION_TEXT = "question_text" + private const val ARG_IS_LAST = "is_last" + private const val TAG = "GadQuestionFragment" + private var isNavigatingToResult = false + + @JvmStatic + fun newInstance(questionNumber: Int, questionText: String, isLast: Boolean = false) = + GadQuestionFragment().apply { + arguments = Bundle().apply { + putInt(ARG_QUESTION_NUMBER, questionNumber) + putString(ARG_QUESTION_TEXT, questionText) + putBoolean(ARG_IS_LAST, isLast) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/HasilAnxietyFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/HasilAnxietyFragment.kt new file mode 100644 index 0000000..ab10545 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/HasilAnxietyFragment.kt @@ -0,0 +1,151 @@ +package com.example.app_skripsi.ui.checkanxiety.formanxiety + +import android.os.Bundle +import android.util.TypedValue +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.example.app_skripsi.R +import com.example.app_skripsi.databinding.FragmentEmotionBinding +import com.example.app_skripsi.databinding.FragmentHasilAnxietyBinding +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class HasilAnxietyFragment : Fragment() { + private var _binding : FragmentHasilAnxietyBinding? = null + private val binding get() = _binding!! + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentHasilAnxietyBinding.inflate(inflater,container,false) + // Inflate the layout for this fragment + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + + // Retrieve data from intent + val totalScore = arguments?.getInt("TOTAL_SCORE", 0) ?: 0 + val emotion = arguments?.getString("EMOTION") ?: "Tidak Diketahui" + val activity = arguments?.getString("ACTIVITY") ?: "Tidak Diketahui" + + // Set current date + val currentDate = SimpleDateFormat("EEEE, d MMMM yyyy", Locale("id", "ID")).format(Date()) + binding.tvDate.text = "Hari: $currentDate" + + // Set emotion and activity + binding.tvEmotion.text = "Emosi: $emotion" + binding.tvActivity.text = "Kegiatan: $activity" + + // Set total score and anxiety level + binding.tvTotalSkor.text = "Total Skor GAD-7: $totalScore" + setAnxietyLevelAndAdvice(totalScore) + + // Back button + binding.btnBack.setOnClickListener { + requireActivity().onBackPressed() + } + + // Optional: Set individual GAD scores if available + setGadScores() + } + + private fun setAnxietyLevelAndAdvice(totalScore: Int) { + val (level, color, advice) = when { + totalScore in 0..4 -> Triple("Minimal", "#4CAF50", + "Tingkat kecemasan Anda minimal. Terus pertahankan kesehatan mental dan gaya hidup sehat.") + totalScore in 5..9 -> Triple("Ringan", "#7986CB", + "Anda mungkin bisa mengelola ini secara mandiri dengan teknik manajemen stres, latihan relaksasi, dan perubahan gaya hidup seperti tidur cukup dan makan makanan sehat.") + totalScore in 10..14 -> Triple("Sedang", "#FF9800", + "Tingkat kecemasan Anda moderat. Pertimbangkan untuk mencari dukungan dari teman, keluarga, atau profesional kesehatan mental.") + else -> Triple("Berat", "#F44336", + "Tingkat kecemasan Anda tinggi. Sangat disarankan untuk segera berkonsultasi dengan profesional kesehatan mental.") + } + + binding.btnAnxietyManagement.text = "Kecemasan $level" + binding.btnAnxietyManagement.backgroundTintList = android.content.res.ColorStateList.valueOf(android.graphics.Color.parseColor(color)) + binding.tvAdvice.text = advice + } + + private fun setGadScores() { + // Ambil semua jawaban GAD dari arguments + val gadScores = (1..7).map { index -> + val score = arguments?.getInt("GAD$index", -1) ?: -1 + "GAD-$index: ${getGadScoreDescription(score)}" + } + + // Clear existing views + binding.layoutGADScores.removeAllViews() + + // Add GAD scores dynamically + gadScores.forEach { score -> + val textView = TextView(requireContext()).apply { + text = score + setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + setTextColor(ContextCompat.getColor(requireContext(), R.color.gray800)) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 8, 0, 8) // Tambahkan margin antar teks + } + } + binding.layoutGADScores.addView(textView) + } + } + + private fun getGadScoreDescription(score: Int): String { + return when (score) { + 0 -> "Tidak Pernah" + 1 -> "Beberapa Hari" + 2 -> "Lebih dari Setengah Hari" + 3 -> "Hampir Setiap Hari" + else -> "Tidak Diketahui" + } + } + + companion object { + fun newInstance( + totalScore: Int, + emotion: String, + activity: String, + gadScores: List? = null + ): HasilAnxietyFragment { + val fragment = HasilAnxietyFragment() + val args = Bundle().apply { + putInt("TOTAL_SCORE", totalScore) + putString("EMOTION", emotion) + putString("ACTIVITY", activity) + + // Optionally add individual GAD scores + gadScores?.forEachIndexed { index, score -> + putInt("GAD${index + 1}", score) + } + } + fragment.arguments = args + return fragment + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/PermissionFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/PermissionFragment.kt new file mode 100644 index 0000000..76dc3dc --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/formanxiety/PermissionFragment.kt @@ -0,0 +1,111 @@ +package com.example.app_skripsi.ui.checkanxiety.formanxiety + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.ViewPager2 +import com.example.app_skripsi.R +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.databinding.FragmentPermissionBinding +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyActivity +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch + + +class PermissionFragment : Fragment() { + private var _binding : FragmentPermissionBinding? = null + private val binding get() = _binding!! + + private lateinit var formSessionManager: FormSessionManager + private val TAG = "PermissionFragment" // Tag untuk logging + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentPermissionBinding.inflate(inflater, container,false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + formSessionManager = FormSessionManager(requireContext()) + + + // Pastikan untuk mendapatkan tipe deteksi LANGSUNG dari activity + val detectionType = (activity as? FormAnxietyActivity)?.detectionType ?: "QUICK" + Log.d(TAG, "Detection type from Activity: $detectionType") + + // Update deskripsi berdasarkan tipe dari activity + updateDescriptionBasedOnType(detectionType) + + // Juga periksa dari DataStore untuk memastikan konsistensi + lifecycleScope.launch { + val storedType = formSessionManager.getDetectionType() + Log.d(TAG, "Detection type from DataStore: $storedType") + + // Jika ada perbedaan, update lagi deskripsi + if (storedType != detectionType) { + Log.d(TAG, "Type mismatch! Updating from DataStore value") + updateDescriptionBasedOnType(storedType) + } + } + +// // Tentukan tipe deteksi dan perbarui deskripsi +// lifecycleScope.launch { +// val detectionType = formSessionManager.getDetectionType() +// updateDescriptionBasedOnType(detectionType) +// +// // Log tipe deteksi saat fragment dibuat +// Log.d(TAG, "Detection type on fragment creation: $detectionType") +// } + + binding.btnLanjutkan.setOnClickListener { + lifecycleScope.launch { + // Log tipe deteksi saat tombol diklik + val detectionType = formSessionManager.getDetectionType() + Log.d(TAG, "Detection type when button clicked: $detectionType") + + // Simpan langkah saat ini sebagai "emotion" untuk navigasi + formSessionManager.saveEmotion("") + + // Pindah ke halaman berikutnya dengan ViewPager2 + val viewPager2 = activity?.findViewById(R.id.fragment_container) + viewPager2?.currentItem = 1 // Pindah ke fragment EmotionFragment (index 1) + } + } + } + + // Fungsi untuk memperbarui deskripsi berdasarkan tipe deteksi + private fun updateDescriptionBasedOnType(detectionType: String) { + if (detectionType == "ROUTINE") { + binding.tvPermissionDescription.text = getString(R.string.routine_detection_description) + Log.d(TAG, "Setting description for ROUTINE detection") + } else { + binding.tvPermissionDescription.text = getString(R.string.quick_detection_description) + Log.d(TAG, "Setting description for QUICK detection") + } + } + + companion object { + + @JvmStatic + fun newInstance(param1: String, param2: String) = + PermissionFragment().apply { + arguments = Bundle().apply { + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionAdapter.kt new file mode 100644 index 0000000..9d7b51b --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionAdapter.kt @@ -0,0 +1,65 @@ +package com.example.app_skripsi.ui.checkanxiety.shortdetection + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.ShortDetectionModel +import com.example.app_skripsi.databinding.ItemRiwayatShortDetectionBinding + +class ShortDetectionAdapter(private val shortDetectionList: List) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShortDetectionViewHolder { + val binding = ItemRiwayatShortDetectionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ShortDetectionViewHolder(binding) + } + + override fun onBindViewHolder(holder: ShortDetectionViewHolder, position: Int) { + val item = shortDetectionList[position] + holder.bind(item) + } + + override fun getItemCount(): Int { + return shortDetectionList.size + } + + class ShortDetectionViewHolder(private val binding: ItemRiwayatShortDetectionBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(item: ShortDetectionModel) { + // Format tanggal dengan SimpleDateFormat jika diperlukan + val formattedDate = item.tanggal.toDate().toString() + + // Bind data ke item layout + binding.tvDate.text = "Hari: ${item.hari}, ${formattedDate}" + binding.tvEmotion.text = "Emosi: ${item.emosi}" + binding.tvActivity.text = "Kegiatan: ${item.kegiatan}" + binding.tvskorGAD.text = "Total Skor GAD 7: ${item.total_skor}" + + // GAD Scores + binding.layoutGADScores.removeAllViews() + val gadScores = listOf( + "GAD-1: ${item.gad1}", + "GAD-2: ${item.gad2}", + "GAD-3: ${item.gad3}", + "GAD-4: ${item.gad4}", + "GAD-5: ${item.gad5}", + "GAD-6: ${item.gad6}", + "GAD-7: ${item.gad7}" + ) + + gadScores.forEach { score -> + val textView = TextView(binding.root.context).apply { + text = score + textSize = 14f + setTypeface(resources.getFont(R.font.inter_regular)) + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + } + binding.layoutGADScores.addView(textView) + } + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionViewModel.kt new file mode 100644 index 0000000..5caad74 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionViewModel.kt @@ -0,0 +1,25 @@ +package com.example.app_skripsi.ui.checkanxiety.shortdetection + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.app_skripsi.data.model.ShortDetectionModel +import com.example.app_skripsi.data.repository.AnxietyRepository + +class ShortDetectionViewModel(private val anxietyRepository: AnxietyRepository) : ViewModel() { + + // MutableLiveData untuk menyimpan list deteksi singkat + private val _shortDetectionList = MutableLiveData>() + val shortDetectionList: LiveData> get() = _shortDetectionList + + // Fungsi untuk mengambil data deteksi singkat + suspend fun fetchShortDetections() { + anxietyRepository.getShortDetections().onSuccess { detections -> + // Memperbarui LiveData dengan hasil deteksi singkat + _shortDetectionList.value = detections + }.onFailure { + // Handle error jika terjadi kegagalan dalam pengambilan data + _shortDetectionList.value = emptyList() // Menyediakan data kosong jika gagal + } + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionViewModelFactory.kt b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionViewModelFactory.kt new file mode 100644 index 0000000..42c70dc --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/checkanxiety/shortdetection/ShortDetectionViewModelFactory.kt @@ -0,0 +1,20 @@ +package com.example.app_skripsi.ui.checkanxiety.shortdetection + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.data.repository.AnxietyRepository +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyViewModel + +class ShortDetectionViewModelFactory(private val anxietyRepository: AnxietyRepository) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(ShortDetectionViewModel::class.java)) { + return ShortDetectionViewModel( + anxietyRepository, + + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardActivity.kt new file mode 100644 index 0000000..a323d07 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardActivity.kt @@ -0,0 +1,213 @@ +package com.example.app_skripsi.ui.dashboard + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.ViewPager2 +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.AppDatabase +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.data.local.user.UserEntity +import com.example.app_skripsi.data.repository.UserRepository +import com.example.app_skripsi.databinding.ActivityDashboardBinding +import com.example.app_skripsi.ui.auth.login.LoginActivity +import com.example.app_skripsi.ui.notification.NotificationViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class DashboardActivity : AppCompatActivity() { + private var _binding : ActivityDashboardBinding? = null + private val binding get() = _binding!! + + private val viewModel: DashboardViewModel by viewModels() + private lateinit var pagerAdapter: DashboardPagerAdapter + + private val userRepository by lazy { + val database = AppDatabase.getDatabase(application) + UserRepository(FirebaseService(), database.userDao()) + } + private val firebaseService by lazy { + FirebaseService() + } + + private val routineSessionManager by lazy { + RoutineSessionManager(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityDashboardBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val bottomNav = insets.getInsets(WindowInsetsCompat.Type.ime()) + v.setPadding(systemBars.left , systemBars.top, + systemBars.right , bottomNav.bottom) + insets + } +// / Hide status bar & navigation bar + hideSystemUI() + // 🔹 Ambil User ID dari intent atau session + lifecycleScope.launch { + val userIdIntent = intent.getStringExtra("USER_ID") ?: "" + SessionManager(this@DashboardActivity).sessionUserId.collect { userId -> + Log.d("DashboardActivity","Try collecting userId from session : $userId") + if (!userId.isNullOrEmpty()) { + // Load user data + loadUserData(userId) + syncRoutineSessionStatus(userId) + } else { + // Redirect to login if no user session is found + startActivity(Intent(this@DashboardActivity, LoginActivity::class.java)) + finish() + } + } + } + setupViewPager() + setupBottomNavigation() + } + + private suspend fun loadUserData(userId: String) { + val localUser = userRepository.getUserFromLocal(userId) + if (localUser != null) { + viewModel.setUserData(localUser.nama, localUser.email) + android.util.Log.d("DashboardActivity", "✅ User Data Loaded from SQLite: ${localUser.nama} - ${localUser.email}") + } else { + android.util.Log.e("DashboardActivity", "⚠️ User not found in SQLite, fetching from Firebase...") + + val firebaseUser = userRepository.getUserFromFirebase(userId) + if (firebaseUser.isSuccess) { + val user = firebaseUser.getOrNull() + if (user != null) { + viewModel.setUserData(user.nama, user.email) + android.util.Log.d("DashboardActivity", "🔥 User Data Loaded from Firebase: ${user.nama} - ${user.email}") + + // 🔹 Simpan ke SQLite untuk caching + userRepository.insertUser( + UserEntity( + userId = userId, + nama = user.nama, + email = user.email, + jenisKelamin = user.jenisKelamin, + umur = user.umur + ) + ) + } + } + } + } + + private suspend fun syncRoutineSessionStatus(userId: String) { + try { + // Set user ID di RoutineSessionManager + routineSessionManager.setUserId(userId) + + // Cek status di DataStore + val isLocalSessionActive = routineSessionManager.isSessionActive.first() + + // Cek status di Firebase + val result = firebaseService.getActiveRoutineDetection(userId) + val isFirebaseSessionActive = result.isSuccess && result.getOrNull() != null + + Log.d("DashboardActivity", "Sinkronisasi sesi rutin - Local: $isLocalSessionActive, Firebase: $isFirebaseSessionActive") + + if (isLocalSessionActive != isFirebaseSessionActive) { + if (isFirebaseSessionActive) { + // Firebase aktif tapi lokal tidak, restore sesi + val success = routineSessionManager.restoreSessionFromFirebase(firebaseService) + Log.d("DashboardActivity", "Memulihkan sesi dari Firebase: $success") + } else if (isLocalSessionActive) { + // Lokal aktif tapi Firebase tidak, akhiri sesi lokal + routineSessionManager.endSession() + Log.d("DashboardActivity", "Mengakhiri sesi lokal karena tidak ada sesi aktif di Firebase") + } + } + + // Verifikasi hari terakhir sesi saat ini + if (isLocalSessionActive || isFirebaseSessionActive) { + checkLastDayOfSession(userId) + } + } catch (e: Exception) { + Log.e("DashboardActivity", "Error saat sinkronisasi sesi rutin: ${e.message}") + } + } + + private suspend fun checkLastDayOfSession(userId: String) { + try { + // Cek apakah hari ini adalah hari terakhir sesi + val isLastDay = routineSessionManager.isTodayLastDayOfSession() + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + + if (isLastDay && hasCompletedToday) { + Log.d("DashboardActivity", "Hari terakhir sesi dan sudah diisi, akan mengakhiri sesi secara otomatis") + + // Akhiri sesi di Firebase + val activeRoutineResult = firebaseService.getActiveRoutineDetection(userId) + if (activeRoutineResult.isSuccess && activeRoutineResult.getOrNull() != null) { + val routineDoc = activeRoutineResult.getOrNull()!! + firebaseService.updateRoutineDetectionStatus(userId, routineDoc.first, false) + + // Akhiri sesi lokal + routineSessionManager.endSession() + + Log.d("DashboardActivity", "Sesi rutin berhasil diakhiri otomatis") + } + } + } catch (e: Exception) { + Log.e("DashboardActivity", "Error saat memeriksa hari terakhir sesi: ${e.message}") + } + } + + private fun setupViewPager() { + pagerAdapter = DashboardPagerAdapter(this) + binding.viewPager.adapter = pagerAdapter + binding.viewPager.isUserInputEnabled = true // Disable swipe navigation + + binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + binding.bottomNavigation.menu.getItem(position).isChecked = true + } + }) + } + + private fun setupBottomNavigation() { + binding.bottomNavigation.setOnItemSelectedListener { item -> + when (item.itemId) { + R.id.nav_home -> binding.viewPager.currentItem = 0 + R.id.nav_check_anxiety -> binding.viewPager.currentItem = 1 + R.id.nav_profile -> binding.viewPager.currentItem = 2 + } + true + } + } + + + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, window.decorView).apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } + + override fun onResume() { + super.onResume() + hideSystemUI() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardPagerAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardPagerAdapter.kt new file mode 100644 index 0000000..f10c5fc --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardPagerAdapter.kt @@ -0,0 +1,23 @@ +package com.example.app_skripsi.ui.dashboard + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.example.app_skripsi.ui.dashboard.checkanxiety.CheckAnxietyFragment +import com.example.app_skripsi.ui.dashboard.home.HomeFragment +import com.example.app_skripsi.ui.dashboard.profile.ProfileFragment + +class DashboardPagerAdapter(activity:FragmentActivity) : FragmentStateAdapter(activity) { + + private val fragments = listOf( + HomeFragment(), + CheckAnxietyFragment(), + ProfileFragment() + ) + override fun getItemCount(): Int { + return fragments.size + } + + override fun createFragment(position: Int): Fragment = fragments[position] + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..c22da0a --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,58 @@ +package com.example.app_skripsi.ui.dashboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.repository.UserRepository +import kotlinx.coroutines.launch + +class DashboardViewModel(): ViewModel() { + private val _userName = MutableLiveData() + val userName: LiveData get() = _userName + + private val _userEmail = MutableLiveData() + val userEmail: LiveData get() = _userEmail + + fun loadUserFromSQLite(userRepository: UserRepository, userId: String) { + viewModelScope.launch { + val localUser = userRepository.getUserFromLocal(userId) + if (localUser != null) { + _userName.postValue(localUser.nama) + _userEmail.postValue(localUser.email) + android.util.Log.d("DashboardViewModel", "✅ Loaded user from SQLite: ${localUser.nama}") + } else { + android.util.Log.e("DashboardViewModel", "⚠️ No user found in SQLite") + } + } + } + + fun setUserData(name: String, email: String) { + _userName.value = name + _userEmail.value = email + } + + fun setUserEmail(email: String) { + _userEmail.value = email + } + + // Fungsi untuk mengambil huruf pertama dari email sebagai avatar + fun getAvatarInitial(): String { + return _userName.value?.firstOrNull()?.lowercase() ?: "U" + } + + fun getUserNameFromEmail(email: String): String { + return email.substringBefore("@").replaceFirstChar { it.uppercaseChar() } + } + + // Fungsi untuk mendapatkan salam sesuai jam saat ini + fun getGreetingMessage(): String { + val currentHour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY) + return when (currentHour) { + in 4..12 -> "Selamat Pagi" + in 12..16 -> "Selamat Siang" + in 16..20 -> "Selamat Sore" + else -> "Selamat Malam" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/checkanxiety/CheckAnxietyFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/checkanxiety/CheckAnxietyFragment.kt new file mode 100644 index 0000000..7e4491c --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/checkanxiety/CheckAnxietyFragment.kt @@ -0,0 +1,267 @@ +package com.example.app_skripsi.ui.dashboard.checkanxiety + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.databinding.FragmentCheckAnxietyBinding +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyActivity +import com.example.app_skripsi.ui.checkanxiety.RoutineResultActivity +import com.example.app_skripsi.utils.NotificationSchedulerManager +import com.example.app_skripsi.utils.ToastUtils +import kotlinx.coroutines.launch + +class CheckAnxietyFragment : Fragment() { + private var _binding: FragmentCheckAnxietyBinding? = null + private val binding get() = _binding!! + private lateinit var routineSessionManager: RoutineSessionManager + private lateinit var firebaseService: FirebaseService + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + // Handle any arguments here if needed + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentCheckAnxietyBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + routineSessionManager = RoutineSessionManager(requireContext()) + firebaseService = FirebaseService() + + // Setup tombol mulai/lanjutkan sesi + binding.btnCheckAnxietyRoutine.setOnClickListener { + handleRoutineButtonClick() + } + + // Setup tombol akhiri sesi + binding.btnEndSessionRoutine.setOnClickListener { + showEndSessionConfirmationDialog() + } + + binding.btnViewRoutineResults.setOnClickListener { + val intent = Intent(requireContext(),RoutineResultActivity::class.java) + startActivity(intent) + } + + // Cek status sesi rutin setiap kali fragment muncul + checkRoutineSessionStatus() + } + + override fun onResume() { + super.onResume() + // Update status sesi ketika fragment kembali muncul + checkRoutineSessionStatus() + } + private fun showEndSessionConfirmationDialog() { + androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("Akhiri Sesi Deteksi Rutin") + .setMessage("Apakah Anda yakin ingin mengakhiri sesi deteksi rutin saat ini? Data yang sudah ada akan tetap tersimpan.") + .setPositiveButton("Ya, Akhiri") { _, _ -> + lifecycleScope.launch { + try { + val success = routineSessionManager.endRoutineSession(firebaseService) + + if (success) { + // Cancel reminders + val notificationManager = NotificationSchedulerManager(requireContext()) + notificationManager.cancelRoutineFormAlarms() + Toast.makeText(requireContext(), + "Sesi deteksi rutin berhasil diakhiri", + Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(requireContext(), + "Gagal mengakhiri sesi deteksi rutin", + Toast.LENGTH_SHORT).show() + } + + // Refresh status setelah operasi + checkRoutineSessionStatus() + } catch (e: Exception) { + Log.e("CheckAnxietyFragment", "Error saat mengakhiri sesi: ${e.message}") + Toast.makeText(requireContext(), + "Terjadi kesalahan saat mengakhiri sesi", + Toast.LENGTH_SHORT).show() + } + } + } + .setNegativeButton("Batal", null) + .show() + } + + private fun handleRoutineButtonClick() { + lifecycleScope.launch { + try { + // Periksa apakah user sudah login + val userId = firebaseService.getCurrentUserId() + if (userId.isNullOrEmpty()) { + Toast.makeText(requireContext(), "Anda belum login", Toast.LENGTH_SHORT).show() + return@launch + } + + // Simpan user ID ke RoutineSessionManager + routineSessionManager.setUserId(userId) + + // Periksa sesi aktif dan status pengisian + val isSessionActive = routineSessionManager.isSessionStillActive() + + if (isSessionActive) { + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + + if (hasCompletedToday) { + ToastUtils.showToast( + requireContext(), + "Anda sudah mengisi form deteksi kecemasan hari ini", + Toast.LENGTH_LONG, + position = ToastUtils.Position.TOP + ) + return@launch + } + } + + // Jika belum mengisi atau tidak ada sesi aktif, buka FormAnxietyActivity + val intent = Intent(requireContext(), FormAnxietyActivity::class.java) + intent.putExtra("DETECTION_TYPE", "ROUTINE") + startActivity(intent) + } catch (e: Exception) { + Log.e("CheckAnxietyFragment", "Error menangani klik tombol: ${e.message}") + Toast.makeText(requireContext(), "Terjadi kesalahan", Toast.LENGTH_SHORT).show() + } + } + } + + private fun checkRoutineSessionStatus() { + lifecycleScope.launch { + try { + // Dapatkan user ID saat ini + val userId = firebaseService.getCurrentUserId() + if (userId == null) { + updateUIForNoSession() + return@launch + } + + // Simpan user ID ke RoutineSessionManager + routineSessionManager.setUserId(userId) + + val isSessionActive = routineSessionManager.isSessionStillActive() + Log.d("CheckAnxietyFragment", "Session active: $isSessionActive") + + if (isSessionActive) { + // Sesi aktif, tampilkan informasi sesi + val sessionType = routineSessionManager.getSessionTypeDisplay() + val currentDay = routineSessionManager.getCurrentSessionDay() + val totalDays = routineSessionManager.getSessionDurationInDays() + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + + Log.d("CheckAnxietyFragment", + "Session info - Type: $sessionType, Day: $currentDay/$totalDays, Completed today: $hasCompletedToday") + + binding.tvTitle.text = "Deteksi Rutin" + binding.tvDescAnxiety.text = "Sesi $sessionType aktif" + binding.tvDayAnxiety.visibility = View.VISIBLE + binding.tvDayAnxiety.text = "Hari $currentDay dari $totalDays" + + // Tampilkan tombol akhiri sesi + binding.btnEndSessionRoutine.visibility = View.VISIBLE + + // Sembunyikan tombol lihat hasil sesi + binding.btnViewRoutineResults.visibility = View.GONE + + if (hasCompletedToday) { + binding.btnCheckAnxietyRoutine.text = "Sudah Diisi Hari Ini" + binding.tvCheckAnxietyPeriodic.text = "Anda sudah mengisi form deteksi kecemasan hari ini" + binding.btnCheckAnxietyRoutine.isEnabled = false + binding.btnCheckAnxietyRoutine.setBackgroundColor( + ContextCompat.getColor(requireContext(), R.color.gray400) + ) + binding.btnCheckAnxietyRoutine.setTextColor( + ContextCompat.getColor(requireContext(), R.color.gray700) + ) + } else { + binding.btnCheckAnxietyRoutine.text = "Lanjutkan Sesi" + binding.btnCheckAnxietyRoutine.isEnabled = true + binding.tvCheckAnxietyPeriodic.text = "Lanjutkan sesi deteksi kecemasan rutin Anda" + binding.btnCheckAnxietyRoutine.setBackgroundColor( + ContextCompat.getColor(requireContext(), R.color.bluePrimary) + ) + binding.btnCheckAnxietyRoutine.setTextColor( + ContextCompat.getColor(requireContext(), R.color.white) + ) + } + + if (currentDay >= totalDays && hasCompletedToday) { + // Hari terakhir dan sudah diisi, akhiri sesi + Log.d("CheckAnxietyFragment", "Last day completed, ending session automatically") + routineSessionManager.endRoutineSession(firebaseService) + + // Refresh UI untuk menampilkan status baru + checkRoutineSessionStatus() + return@launch + } + + } else { + updateUIForNoSession() + } + } catch (e: Exception) { + Log.e("CheckAnxietyFragment", "Error checking routine session status", e) + updateUIForNoSession() + } + } + } + + private fun updateUIForNoSession() { + binding.tvTitle.text = "Deteksi Rutin" + binding.tvDescAnxiety.text = "Anda belum memulai sesi Deteksi Rutin !" + binding.tvDayAnxiety.visibility = View.GONE + + binding.btnCheckAnxietyRoutine.text = "Mulai Sesi" + binding.btnCheckAnxietyRoutine.isEnabled = true + binding.tvCheckAnxietyPeriodic.text = "Apakah anda ingin memulai sesi rutin untuk deteksi kecemasan anda?" + binding.btnCheckAnxietyRoutine.setBackgroundColor( + ContextCompat.getColor(requireContext(), R.color.bluePrimary) + ) + binding.btnCheckAnxietyRoutine.setTextColor( + ContextCompat.getColor(requireContext(), R.color.white) + ) + + // Sembunyikan tombol akhiri sesi + binding.btnEndSessionRoutine.visibility = View.GONE + + // Tampilkan tombol lihat hasil sesi + binding.btnViewRoutineResults.visibility = View.VISIBLE + } + + + + companion object { + @JvmStatic + fun newInstance(param1: String, param2: String) = + CheckAnxietyFragment().apply { + arguments = Bundle().apply { + // Setup arguments if needed + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/home/HomeFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/home/HomeFragment.kt new file mode 100644 index 0000000..597b775 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/home/HomeFragment.kt @@ -0,0 +1,208 @@ +package com.example.app_skripsi.ui.dashboard.home + +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.R +import com.example.app_skripsi.databinding.FragmentHomeBinding +import com.example.app_skripsi.ui.dashboard.DashboardViewModel +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.model.VideoModel +import com.example.app_skripsi.data.repository.VideoRepository +import com.example.app_skripsi.ui.checkanxiety.FormAnxietyActivity +import com.example.app_skripsi.ui.detailvideo.DetailVideoActivity +import com.example.app_skripsi.ui.profile.VideoListActivity +import com.example.app_skripsi.ui.video.VideoViewModel +import com.example.app_skripsi.ui.detailvideo.VideoViewModelFactory +import com.example.app_skripsi.utils.ToastUtils +import kotlinx.coroutines.launch +import android.util.Log + +class HomeFragment : Fragment() { + private var _binding: FragmentHomeBinding? = null + private val binding get() = _binding!! + + private val viewModel: DashboardViewModel by lazy { + ViewModelProvider(requireActivity())[DashboardViewModel::class.java] + } + + private val firebaseService by lazy { FirebaseService() } + private val videoRepository by lazy { VideoRepository(firebaseService) } + private val videoViewModel: VideoViewModel by viewModels { + VideoViewModelFactory(videoRepository) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Setup greeting and UI + setupUserGreeting() + setupAnxietyCheckButtons() + setupRecyclerView() + observeVideoData() + + // Load all videos + videoViewModel.loadAllVideos() + } + + private fun setupUserGreeting() { + viewModel.userName.observe(viewLifecycleOwner) { name -> + if (!name.isNullOrEmpty()) { + binding.tvGreeting.text = "${viewModel.getGreetingMessage()}, $name" + binding.tvInitials.text = name.firstOrNull()?.uppercase() ?: "U" + Log.d("HomeFragment", "✅ Updated UI with name: $name") + } else { + Log.e("HomeFragment", "⚠️ Name is empty, using default") + binding.tvGreeting.text = "${viewModel.getGreetingMessage()}, User" + binding.tvInitials.text = "U" + } + } + } + + private fun setupAnxietyCheckButtons() { + binding.btnCheckAnxietyQuick.setOnClickListener { + // Reset sesi sebelum memulai deteksi baru + val formSessionManager = FormSessionManager(requireContext()) + lifecycleScope.launch { + formSessionManager.resetSession() + + val intent = Intent(requireContext(), FormAnxietyActivity::class.java) + intent.putExtra("DETECTION_TYPE", "QUICK") + startActivity(intent) + } + } + + binding.btnCheckAnxietyRoutine.setOnClickListener { + lifecycleScope.launch { + // Periksa dulu apakah sudah mengisi hari ini + val routineSessionManager = RoutineSessionManager(requireContext()) + val isSessionActive = routineSessionManager.isSessionStillActive() + + if (isSessionActive) { + val hasCompletedToday = routineSessionManager.hasCompletedFormToday() + + if (hasCompletedToday) { + // Sudah mengisi form hari ini, tampilkan pesan + ToastUtils.showToast( + requireContext(), + "Anda sudah mengisi form deteksi kecemasan hari ini", + Toast.LENGTH_LONG, + position = ToastUtils.Position.TOP + ) + return@launch + } + } + + // Jika belum mengisi, lanjutkan dengan reset session dan buka FormAnxietyActivity + val formSessionManager = FormSessionManager(requireContext()) + formSessionManager.resetSession() + + val intent = Intent(requireContext(), FormAnxietyActivity::class.java) + intent.putExtra("DETECTION_TYPE", "ROUTINE") + startActivity(intent) + } + } + } + + private fun setupRecyclerView() { + binding.recyclerViewVideo.layoutManager = LinearLayoutManager(requireContext()) + + // Tambahkan onClick listener untuk "Video untuk anda" jika ingin mengarahkan ke VideoListActivity + binding.tvVideoRekomendasi.setOnClickListener { + val intent = Intent(requireContext(), VideoListActivity::class.java) + startActivity(intent) + } + } + + private fun observeVideoData() { + // Observe loading state + videoViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> + if (isLoading) { + binding.progressBar.visibility = View.VISIBLE + } else { + binding.progressBar.visibility = View.GONE + } + } + + // Observe videos data + videoViewModel.videos.observe(viewLifecycleOwner) { videos -> + Log.d("HomeFragment", "Loaded ${videos.size} videos") + + if (videos.isNotEmpty()) { + val videoAdapter = VideoAdapter(videos) { video -> + val intent = Intent(requireContext(), DetailVideoActivity::class.java) + intent.putExtra("VIDEO_MODEL", video) + startActivity(intent) + } + + binding.recyclerViewVideo.adapter = videoAdapter + } else { + // Jika tidak ada video, tampilkan pesan + showNoVideosMessage() + } + } + + // Observe error state + videoViewModel.error.observe(viewLifecycleOwner) { errorMessage -> + errorMessage?.let { + Log.e("HomeFragment", "Error loading videos: $it") + + // Tampilkan pesan error khusus untuk masalah izin + if (it.contains("PERMISSION_DENIED")) { + Toast.makeText( + requireContext(), + "Tidak dapat mengakses data video. Silakan cek aturan keamanan Firestore.", + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText(requireContext(), "Gagal memuat video: $it", Toast.LENGTH_SHORT).show() + } + + videoViewModel.clearError() + + // Tampilkan UI kosong + showNoVideosMessage() + } + } + } + + private fun showNoVideosMessage() { + // Tampilkan pesan bahwa tidak ada video yang tersedia + Toast.makeText( + requireContext(), + "Tidak ada video tersedia saat ini", + Toast.LENGTH_SHORT + ).show() + + // Opsional: Tampilkan UI kosong jika Anda memiliki view untuk itu + binding.tvEmptyMessage.visibility = View.VISIBLE + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + @JvmStatic + fun newInstance() = HomeFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/home/VideoAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/home/VideoAdapter.kt new file mode 100644 index 0000000..b39ddb0 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/home/VideoAdapter.kt @@ -0,0 +1,43 @@ +package com.example.app_skripsi.ui.dashboard.home + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.VideoModel +import com.example.app_skripsi.databinding.ItemVideoBinding + +class VideoAdapter( + private val videoList: List, + private val onItemClick: (VideoModel) -> Unit +) : RecyclerView.Adapter() { + + inner class VideoViewHolder(val binding: ItemVideoBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder { + val binding = ItemVideoBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return VideoViewHolder(binding) + } + + override fun onBindViewHolder(holder: VideoViewHolder, position: Int) { + val video = videoList[position] + holder.binding.tvTitle.text = video.title + + // Set thumbnail + if (video.thumbnailUrl != 0) { + holder.binding.imgThumbnail.setImageResource(video.thumbnailUrl) + } else { + // Default thumbnail + holder.binding.imgThumbnail.setImageResource(R.drawable.video_thumbnail) + } + + // Add category badge if needed +// holder.binding.tvCategory.text = video.category + + holder.binding.root.setOnClickListener { onItemClick(video) } + } + + override fun getItemCount(): Int { + return videoList.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/dashboard/profile/ProfileFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/dashboard/profile/ProfileFragment.kt new file mode 100644 index 0000000..32f55c7 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/dashboard/profile/ProfileFragment.kt @@ -0,0 +1,147 @@ +package com.example.app_skripsi.ui.dashboard.profile + +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.local.SessionManager +import com.example.app_skripsi.data.local.AppDatabase +import com.example.app_skripsi.data.local.FormSessionManager +import com.example.app_skripsi.data.local.RoutineSessionManager +import com.example.app_skripsi.data.local.user.toUserModel +import com.example.app_skripsi.data.repository.UserRepository +import com.example.app_skripsi.databinding.FragmentProfileBinding +import com.example.app_skripsi.ui.auth.login.LoginActivity +import com.example.app_skripsi.ui.dashboard.DashboardViewModel +import com.example.app_skripsi.ui.profile.AboutActivity +import com.example.app_skripsi.ui.profile.diary.DiaryActivity +import com.example.app_skripsi.ui.profile.faq.FaqActivity +import com.example.app_skripsi.ui.profile.RiwayatCheckActivity +import com.example.app_skripsi.ui.profile.VideoListActivity +import com.example.app_skripsi.utils.NotificationSchedulerManager +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class ProfileFragment : Fragment() { + private var _binding: FragmentProfileBinding? = null + private val binding get() = _binding!! + + // Ambil ViewModel dari DashboardActivity + private val viewModel: DashboardViewModel by lazy { + ViewModelProvider(requireActivity())[DashboardViewModel::class.java] + } + private val sessionManager by lazy { SessionManager(requireContext()) } + + // ✅ Initialize userRepository properly + private val userRepository by lazy { + val database = AppDatabase.getDatabase(requireContext()) + UserRepository(FirebaseService(), database.userDao()) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Observe email user dari ViewModel agar selalu update + viewModel.userName.observe(viewLifecycleOwner) { name -> + binding.profileName.text = viewModel.getUserNameFromEmail(name) + binding.profileInitial.text = viewModel.getAvatarInitial() + } + + viewModel.userEmail.observe(viewLifecycleOwner) { email -> + binding.profileEmail.text = email + } + + setupNavigation() + } + + private fun setupNavigation() { + binding.menuDiary.setOnClickListener { + startActivity(Intent(requireContext(), DiaryActivity::class.java)) + } + binding.menuVideos.setOnClickListener { + startActivity(Intent(requireContext(), VideoListActivity::class.java)) + } + binding.menuHistory.setOnClickListener { + startActivity(Intent(requireContext(), RiwayatCheckActivity::class.java)) + } + binding.menuFaq.setOnClickListener { + startActivity(Intent(requireContext(), FaqActivity::class.java)) + } + binding.menuAbout.setOnClickListener { + startActivity(Intent(requireContext(), AboutActivity::class.java)) + } + // Di dalam ProfileFragment.kt, perbarui bagian handleLogout/menuLogout: + binding.menuLogout.setOnClickListener { + viewLifecycleOwner.lifecycleScope.launch { + val userId = sessionManager.sessionUserId.first() + if (userId == null) { + android.util.Log.e("LogoutFlow", "❌ sessionUserId is NULL, cannot logout") + return@launch + } + + // ✅ Check for data changes in SQLite before logging out + val localUser = userRepository.getUserFromLocal(userId) + val firebaseUser = userRepository.getUserFromFirebase(userId) + + if (localUser != null && firebaseUser.isSuccess) { + val firebaseData = firebaseUser.getOrNull() + if (firebaseData != localUser.toUserModel()) { + android.util.Log.d("LogoutFlow", "🔄 Data has changed, syncing to Firebase...") + + // ✅ Sync to Firebase + val updateResult = userRepository.updateUserToFirebase(localUser) + if (updateResult.isSuccess) { + android.util.Log.d("LogoutFlow", "✅ Data Synced to Firebase before logout") + } else { + android.util.Log.e("LogoutFlow", "❌ Failed to sync data to Firebase before logout") + } + } else { + android.util.Log.d("LogoutFlow", "🔹 No changes detected, skipping Firebase sync") + } + } + + // Cancel semua notifikasi saat logout + val notificationManager = NotificationSchedulerManager(requireContext()) + notificationManager.cancelRoutineFormAlarms() + notificationManager.cancelAlarmNotifications() + + // Bersihkan data RoutineSessionManager + val routineSessionManager = RoutineSessionManager(requireContext()) + routineSessionManager.clearAllData() // Pastikan method ini ada di RoutineSessionManager + + // Bersihkan data FormSessionManager + val formSessionManager = FormSessionManager(requireContext()) + formSessionManager.resetSession() + + // ✅ Clear session & SQLite + sessionManager.clearSession() + userRepository.clearLocalDatabase() + + android.util.Log.d("LogoutFlow", "🗑 SQLite Cleared & Session Ended") + + val intent = Intent(requireContext(), LoginActivity::class.java) + startActivity(intent) + requireActivity().finish() + } + } + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/detailvideo/DetailVideoActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/detailvideo/DetailVideoActivity.kt new file mode 100644 index 0000000..78b8442 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/detailvideo/DetailVideoActivity.kt @@ -0,0 +1,157 @@ +package com.example.app_skripsi.ui.detailvideo + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util +import androidx.media3.exoplayer.ExoPlayer +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.VideoModel +import com.example.app_skripsi.databinding.ActivityDetailVideoBinding + +class DetailVideoActivity : AppCompatActivity() { + private var _binding: ActivityDetailVideoBinding? = null + private val binding get() = _binding!! + + private var player: ExoPlayer? = null + private var playWhenReady = true + private var currentPosition = 0L + private var playbackPosition = 0L + + private var videoModel: VideoModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityDetailVideoBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left+v.paddingLeft, systemBars.top + v.paddingTop, + systemBars.right +v.paddingRight, systemBars.bottom+v.paddingBottom) + insets + } + + // Hide system UI + hideSystemUI() + + // Get video data from intent + videoModel = intent.getParcelableExtra("VIDEO_MODEL") + + if (videoModel == null) { + Toast.makeText(this, "Video tidak ditemukan", Toast.LENGTH_SHORT).show() + finish() + return + } + + // Set video data to views + binding.tvDetailTitle.text = videoModel?.title + binding.tvDescription.text = videoModel?.description + + // Show source if available + if (!videoModel?.sourceUrl.isNullOrEmpty() && videoModel?.hasCopyright == true) { + binding.tvSource.visibility = View.VISIBLE + binding.tvSource.text = "Sumber: ${videoModel?.sourceUrl}" + } else { + binding.tvSource.visibility = View.GONE + } + + binding.btnBack.setOnClickListener { finish() } + } + + private fun initializePlayer() { + val videoUrl = videoModel?.videoUrl + + if (videoUrl.isNullOrEmpty()) { + Toast.makeText(this, "URL video tidak tersedia", Toast.LENGTH_SHORT).show() + return + } + + player = ExoPlayer.Builder(this) + .build() + .also { exoPlayer -> + binding.playerView.player = exoPlayer + + val mediaItem = MediaItem.fromUri(Uri.parse(videoUrl)) + exoPlayer.setMediaItem(mediaItem) + + exoPlayer.playWhenReady = playWhenReady + exoPlayer.seekTo(playbackPosition) + exoPlayer.prepare() + + // Add player listener + exoPlayer.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_READY) { + binding.progressBarLoading.visibility = View.GONE + } else if (playbackState == Player.STATE_BUFFERING) { + binding.progressBarLoading.visibility = View.VISIBLE + } + } + }) + } + } + + private fun releasePlayer() { + player?.let { exoPlayer -> + playbackPosition = exoPlayer.currentPosition + playWhenReady = exoPlayer.playWhenReady + exoPlayer.release() + } + player = null + } + + @OptIn(UnstableApi::class) + override fun onStart() { + super.onStart() + if (Util.SDK_INT >= 24) { + initializePlayer() + } + } + + @OptIn(UnstableApi::class) + override fun onResume() { + super.onResume() + if (Util.SDK_INT < 24 || player == null) { + initializePlayer() + } + } + + @OptIn(UnstableApi::class) + override fun onPause() { + super.onPause() + if (Util.SDK_INT < 24) { + releasePlayer() + } + } + + @OptIn(UnstableApi::class) + override fun onStop() { + super.onStop() + if (Util.SDK_INT >= 24) { + releasePlayer() + } + } + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, window.decorView).apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/detailvideo/VideoViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/detailvideo/VideoViewModel.kt new file mode 100644 index 0000000..f16b54e --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/detailvideo/VideoViewModel.kt @@ -0,0 +1,60 @@ +package com.example.app_skripsi.ui.video + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.model.VideoModel +import com.example.app_skripsi.data.repository.VideoRepository +import kotlinx.coroutines.launch + +class VideoViewModel(private val videoRepository: VideoRepository) : ViewModel() { + + private val _videos = MutableLiveData>() + val videos: LiveData> = _videos + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _error = MutableLiveData() + val error: LiveData = _error + + // Metode utama untuk memuat video + fun loadAllVideos() { + viewModelScope.launch { + _isLoading.value = true + _error.value = null + + try { + val result = videoRepository.getAllVideos() + + result.fold( + onSuccess = { videoList -> + _videos.value = videoList + Log.d("VideoViewModel", "Loaded ${videoList.size} videos") + }, + onFailure = { exception -> + _error.value = exception.message + Log.e("VideoViewModel", "Error loading videos: ${exception.message}") + } + ) + } catch (e: Exception) { + _error.value = e.message + Log.e("VideoViewModel", "Exception: ${e.message}", e) + } finally { + _isLoading.value = false + } + } + } + + // Tetap sediakan metode ini untuk kompatibilitas + fun loadVideosByCategory(category: String) { + // Tetap menggunakan loadAllVideos() karena tidak ada kategori + loadAllVideos() + } + + fun clearError() { + _error.value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/detailvideo/VideoViewModelFactory.kt b/app/src/main/java/com/example/app_skripsi/ui/detailvideo/VideoViewModelFactory.kt new file mode 100644 index 0000000..8aa6907 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/detailvideo/VideoViewModelFactory.kt @@ -0,0 +1,17 @@ +package com.example.app_skripsi.ui.detailvideo + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.data.repository.VideoRepository +import com.example.app_skripsi.ui.video.VideoViewModel + +class VideoViewModelFactory(private val videoRepository: VideoRepository) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(VideoViewModel::class.java)) { + return VideoViewModel(videoRepository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/notification/NotificationViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/notification/NotificationViewModel.kt new file mode 100644 index 0000000..65a2e5a --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/notification/NotificationViewModel.kt @@ -0,0 +1,64 @@ +package com.example.app_skripsi.ui.notification + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.work.* +import com.example.app_skripsi.workers.DailyNotificationWorker +import com.example.app_skripsi.workers.PeriodicNotificationWorker +import java.util.concurrent.TimeUnit + +class NotificationViewModel(application: Application) : AndroidViewModel(application) { + + fun scheduleDailyNotification() { + try { + // Membuat dan menjadwalkan pekerjaan untuk menampilkan notifikasi + val workRequest = OneTimeWorkRequestBuilder() + .setInitialDelay(calculateInitialDelay(), TimeUnit.MILLISECONDS) + .build() + + // Menjadwalkan pekerjaan menggunakan WorkManager + WorkManager.getInstance(getApplication()).enqueue(workRequest) + Log.d("NotificationViewModel", "Notifikasi workmanager telah dibuat ${workRequest.tags}") + + } catch (e: Exception) { + Log.e("NotificationViewModel", "Notifikasi workmanager gagal di buat", e) + } + } + + fun schedulePeriodicNotification() { + try { + // Membuat dan menjadwalkan pekerjaan untuk notifikasi periodik + val workRequest = PeriodicWorkRequestBuilder(2, TimeUnit.HOURS) + .setInitialDelay(0, TimeUnit.MILLISECONDS) // Tidak ada delay awal, langsung mulai + .build() + + // Menjadwalkan pekerjaan menggunakan WorkManager + WorkManager.getInstance(getApplication()).enqueue(workRequest) + Log.d("NotificationViewModel", "Notifikasi periodik telah dibuat ${workRequest.tags}") + + } catch (e: Exception) { + Log.e("NotificationViewModel", "Notifikasi periodik gagal dibuat", e) + } + } + + private fun calculateInitialDelay(): Long { + // Menghitung delay untuk menyesuaikan waktu tertentu (misal, 9 pagi setiap hari) + val calendar = java.util.Calendar.getInstance() + val targetTime = calendar.apply { + set(java.util.Calendar.HOUR_OF_DAY, 5) // Jam 9 pagi + set(java.util.Calendar.MINUTE, 23) + set(java.util.Calendar.SECOND, 0) + }.timeInMillis + + val currentTime = System.currentTimeMillis() + return if (targetTime > currentTime) { + targetTime - currentTime + + } else { + // Jika waktu sudah lewat hari ini, atur untuk besok + targetTime + TimeUnit.DAYS.toMillis(1) - currentTime + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/AboutActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/AboutActivity.kt new file mode 100644 index 0000000..3795063 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/AboutActivity.kt @@ -0,0 +1,45 @@ +package com.example.app_skripsi.ui.profile + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.example.app_skripsi.R +import com.example.app_skripsi.databinding.ActivityAboutBinding + +class AboutActivity : AppCompatActivity() { + private var _binding : ActivityAboutBinding? = null + private val binding get() = _binding!! + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityAboutBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val bottomNav = insets.getInsets(WindowInsetsCompat.Type.ime()) + v.setPadding(systemBars.left+v.paddingLeft , systemBars.top + v.paddingTop, + systemBars.right + v.paddingRight , systemBars.bottom + v.paddingBottom) + insets + } +// / Hide status bar & navigation bar + hideSystemUI() + binding.btnBack.setOnClickListener { + finish() + } + } + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, window.decorView).apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/RiwayatCheckActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/RiwayatCheckActivity.kt new file mode 100644 index 0000000..ace240e --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/RiwayatCheckActivity.kt @@ -0,0 +1,79 @@ +package com.example.app_skripsi.ui.profile + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.app_skripsi.databinding.ActivityRiwayatCheckBinding +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.repository.AnxietyRepository +import com.example.app_skripsi.ui.checkanxiety.shortdetection.ShortDetectionAdapter +import com.example.app_skripsi.ui.checkanxiety.shortdetection.ShortDetectionViewModel +import com.example.app_skripsi.ui.checkanxiety.shortdetection.ShortDetectionViewModelFactory +import kotlinx.coroutines.launch + +class RiwayatCheckActivity : AppCompatActivity() { + + private var _binding: ActivityRiwayatCheckBinding? = null + private val binding get() = _binding!! + + // Mendeklarasikan ViewModel menggunakan delegasi viewModels + private lateinit var firebaseService: FirebaseService + private val shortDetectionViewModel: ShortDetectionViewModel by viewModels { + // Pastikan firebaseService sudah diinisialisasi di sini + ShortDetectionViewModelFactory(AnxietyRepository(firebaseService)) // Pastikan untuk menyuplai AnxietyRepository + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + _binding = ActivityRiwayatCheckBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Inisialisasi firebaseService + firebaseService = FirebaseService() // Atau FirebaseService.getInstance(), sesuai cara kamu menginisialisasinya + + // Setup RecyclerView + val recyclerView = binding.recyclerViewVideo + recyclerView.layoutManager = LinearLayoutManager(this) + + // Observe LiveData dari ViewModel untuk mendapatkan data deteksi singkat + shortDetectionViewModel.shortDetectionList.observe(this) { shortDetectionList -> + // Mengatur visibilitas berdasarkan ada atau tidaknya data + if (shortDetectionList.isNotEmpty()) { + // Menyembunyikan ProgressBar dan menampilkan RecyclerView + binding.progressBar.visibility = android.view.View.GONE + binding.recyclerViewVideo.visibility = android.view.View.VISIBLE + binding.tvNoData.visibility = android.view.View.GONE + + // Mengatur adapter untuk RecyclerView + val adapter = ShortDetectionAdapter(shortDetectionList) + recyclerView.adapter = adapter + } else { + // Jika tidak ada data, menampilkan pesan kosong + binding.progressBar.visibility = android.view.View.GONE + binding.tvNoData.visibility = android.view.View.VISIBLE + binding.recyclerViewVideo.visibility = android.view.View.GONE + } + } + + // Menampilkan ProgressBar saat data sedang dimuat + binding.progressBar.visibility = android.view.View.VISIBLE + binding.recyclerViewVideo.visibility = android.view.View.GONE + binding.tvNoData.visibility = android.view.View.GONE + + // Meminta ViewModel untuk mengambil data deteksi singkat + lifecycleScope.launch { + shortDetectionViewModel.fetchShortDetections() // Memanggil fungsi suspend dalam ViewModel + } + binding.btnBack.setOnClickListener { + finish() + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/VideoListActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/VideoListActivity.kt new file mode 100644 index 0000000..7f741a2 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/VideoListActivity.kt @@ -0,0 +1,129 @@ +package com.example.app_skripsi.ui.profile + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.repository.VideoRepository +import com.example.app_skripsi.databinding.ActivityVideoListBinding +import com.example.app_skripsi.ui.dashboard.home.VideoAdapter +import com.example.app_skripsi.ui.detailvideo.DetailVideoActivity +import com.example.app_skripsi.ui.detailvideo.VideoViewModelFactory +import com.example.app_skripsi.ui.video.VideoViewModel +import android.util.Log + +class VideoListActivity : AppCompatActivity() { + private var _binding : ActivityVideoListBinding? = null + private val binding get() = _binding!! + + private val firebaseService by lazy { FirebaseService() } + private val videoRepository by lazy { VideoRepository(firebaseService) } + private val viewModel: VideoViewModel by viewModels { + VideoViewModelFactory(videoRepository) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityVideoListBinding.inflate(layoutInflater) + setContentView(binding.root) + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val bottomNav = insets.getInsets(WindowInsetsCompat.Type.ime()) + v.setPadding(systemBars.left+v.paddingLeft , systemBars.top + v.paddingTop, + systemBars.right + v.paddingRight , systemBars.bottom + v.paddingBottom) + insets + } + + // Hide status bar & navigation bar + hideSystemUI() + + // Setup RecyclerView + setupRecyclerView() + + // PENTING: Panggil metode untuk mengobservasi perubahan data + observeViewModel() + + // Tentukan kategori dari intent jika ada, atau load semua video + val category = intent.getStringExtra("VIDEO_CATEGORY") + if (category != null) { + binding.tvTitle.text = "Video $category" + viewModel.loadVideosByCategory(category) + } else { + binding.tvTitle.text = "Semua Video" + viewModel.loadAllVideos() + } + + binding.btnBack.setOnClickListener { finish() } + } + + private fun setupRecyclerView() { + binding.recyclerViewVideo.layoutManager = LinearLayoutManager(this) + } + + private fun observeViewModel() { + // Observe loading state + viewModel.isLoading.observe(this) { isLoading -> + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + } + + // Observe videos data + viewModel.videos.observe(this) { videos -> + Log.d("VideoListActivity", "Received ${videos.size} videos from ViewModel") + + if (videos.isNotEmpty()) { + val videoAdapter = VideoAdapter(videos) { video -> + val intent = Intent(this, DetailVideoActivity::class.java) + intent.putExtra("VIDEO_MODEL", video) + startActivity(intent) + } + binding.recyclerViewVideo.adapter = videoAdapter + } else { + // Tampilkan pesan tidak ada video + Toast.makeText(this, "Tidak ada video tersedia", Toast.LENGTH_SHORT).show() + } + } + + // Observe error state + viewModel.error.observe(this) { errorMessage -> + errorMessage?.let { + Log.e("VideoListActivity", "Error loading videos: $it") + + // Tampilkan pesan error khusus untuk masalah izin + if (it.contains("PERMISSION_DENIED")) { + Toast.makeText( + this, + "Tidak dapat mengakses data video. Silakan cek aturan keamanan Firestore.", + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText(this, "Gagal memuat video: $it", Toast.LENGTH_SHORT).show() + } + + viewModel.clearError() + } + } + } + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, window.decorView).apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryActivity.kt new file mode 100644 index 0000000..49f662f --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryActivity.kt @@ -0,0 +1,49 @@ +package com.example.app_skripsi.ui.profile.diary + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.repository.DiaryRepository +import com.example.app_skripsi.databinding.ActivityDiaryBinding +import com.example.app_skripsi.ui.profile.diary.fragments.CreateDiaryFragment +import com.example.app_skripsi.ui.profile.diary.fragments.DiaryListFragment + +class DiaryActivity : AppCompatActivity() { + private var _binding : ActivityDiaryBinding? = null + private val binding get() = _binding!! + + private val viewModel: DiaryViewModel by viewModels { + DiaryViewModelFactory(DiaryRepository(FirebaseService())) + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityDiaryBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + // Set fragment pertama (List Diary) + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, DiaryListFragment()) + .commit() + } + + fun navigateToCreateDiary() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, CreateDiaryFragment()) + .addToBackStack(null) // Agar bisa kembali dengan tombol back + .commit() + } + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryViewModel.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryViewModel.kt new file mode 100644 index 0000000..8d21a85 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryViewModel.kt @@ -0,0 +1,45 @@ +package com.example.app_skripsi.ui.profile.diary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.app_skripsi.data.model.DiaryModel +import com.example.app_skripsi.data.repository.DiaryRepository +import kotlinx.coroutines.launch + +class DiaryViewModel(private val repository: DiaryRepository) : ViewModel() { + private val _selectedEmotion = MutableLiveData() + val selectedEmotion: LiveData get() = _selectedEmotion + + private val _diaries = MutableLiveData>() + val diaries: LiveData> get() = _diaries + + private val _diarySaveResult = MutableLiveData>() + val diarySaveResult: LiveData> get() = _diarySaveResult + + private val _loading = MutableLiveData() + val loading: LiveData get() = _loading + + fun setSelectedEmotion(emotion: String) { + _selectedEmotion.value = emotion + } + + fun fetchDiaries() { + viewModelScope.launch { + _loading.value = true + val result = repository.getDiaries() + if (result.isSuccess) { + _diaries.value = result.getOrDefault(emptyList()) + } + _loading.value = false + } + } + + fun addDiary(diary: DiaryModel) { + viewModelScope.launch { + val result = repository.addDiary(diary) + _diarySaveResult.value = result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryViewModelFactory.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryViewModelFactory.kt new file mode 100644 index 0000000..12764bb --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/DiaryViewModelFactory.kt @@ -0,0 +1,16 @@ +package com.example.app_skripsi.ui.profile.diary + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.data.repository.DiaryRepository + +class DiaryViewModelFactory(private val diaryRepository: DiaryRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return when { + modelClass.isAssignableFrom(DiaryViewModel::class.java) -> { + DiaryViewModel(diaryRepository) as T + } + else -> throw IllegalArgumentException("Unknown ViewModel class") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/adapter/DiaryAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/adapter/DiaryAdapter.kt new file mode 100644 index 0000000..fa8784a --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/adapter/DiaryAdapter.kt @@ -0,0 +1,61 @@ +package com.example.app_skripsi.ui.profile.diary.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.example.app_skripsi.data.model.DiaryModel +import com.example.app_skripsi.databinding.ItemBukuHarianBinding +import com.example.app_skripsi.utils.EmotionUtils +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class DiaryAdapter(private var diaryList: List) : + RecyclerView.Adapter() { + + class DiaryViewHolder(val binding: ItemBukuHarianBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiaryViewHolder { + val binding = ItemBukuHarianBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return DiaryViewHolder(binding) + } + + override fun onBindViewHolder(holder: DiaryViewHolder, position: Int) { + val diary = diaryList[position] + + // Ambil data dari `isiDiary` + val title = diary.isiDiary["judul"] as? String ?: "Tanpa Judul" + val content = diary.isiDiary["isi"] as? String ?: "Tidak ada isi" + val activity = diary.isiDiary["kegiatan"] as? String ?: "Tidak diketahui" + val emotion = diary.isiDiary["emosi"] as? String ?: "Normal" + + // Format timestamp ke tanggal yang mudah dibaca + val formattedDate = formatTimestamp(diary.tanggal.seconds) + + // Set data ke tampilan + holder.binding.tvDate.text = formattedDate + holder.binding.tvTitle.text = title + holder.binding.tvContent.text = content + holder.binding.tvActivityType.text = activity + holder.binding.ivEmotion.setImageResource(EmotionUtils.getEmotionIcon(emotion)) + } + + override fun getItemCount(): Int = diaryList.size + + /** 🔹 Fungsi untuk memperbarui data dengan `DiffUtil` */ + fun updateData(newDiaryList: List) { + val diffCallback = DiaryDiffCallback(diaryList, newDiaryList) + val diffResult = DiffUtil.calculateDiff(diffCallback) + + diaryList = newDiaryList + diffResult.dispatchUpdatesTo(this) // 🔥 Perbarui UI hanya untuk item yang berubah + } + + /** 🔹 Fungsi untuk mengubah Timestamp ke format tanggal */ + private fun formatTimestamp(timestampSeconds: Long): String { + val sdf = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()) + val date = Date(timestampSeconds * 1000) // Ubah detik ke milidetik + return sdf.format(date) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/adapter/DiaryDiffCallback.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/adapter/DiaryDiffCallback.kt new file mode 100644 index 0000000..86dd91d --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/adapter/DiaryDiffCallback.kt @@ -0,0 +1,20 @@ +package com.example.app_skripsi.ui.profile.diary.adapter + +import androidx.recyclerview.widget.DiffUtil +import com.example.app_skripsi.data.model.DiaryModel + +class DiaryDiffCallback( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldList.size + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition].tanggal == newList[newItemPosition].tanggal + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] // Data class sudah memiliki equals() + } +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/fragments/CreateDiaryFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/fragments/CreateDiaryFragment.kt new file mode 100644 index 0000000..1eac32a --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/fragments/CreateDiaryFragment.kt @@ -0,0 +1,179 @@ +package com.example.app_skripsi.ui.profile.diary.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.ViewModelProvider +import com.example.app_skripsi.R +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.model.DiaryModel +import com.example.app_skripsi.data.repository.DiaryRepository +import com.example.app_skripsi.databinding.FragmentCreateDiaryBinding +import com.example.app_skripsi.ui.profile.diary.DiaryViewModel +import com.example.app_skripsi.ui.profile.diary.DiaryViewModelFactory +import com.example.app_skripsi.utils.ToastUtils +import androidx.fragment.app.viewModels + + + +class CreateDiaryFragment : Fragment() { + private var _binding: FragmentCreateDiaryBinding? = null + private val binding get() = _binding!! + private val viewModel: DiaryViewModel by viewModels { + DiaryViewModelFactory(DiaryRepository(FirebaseService())) + } + + private lateinit var emotionMap: Map + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentCreateDiaryBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.btnBack.setOnClickListener { + requireActivity().finish() + } + emotionMap = mapOf( + binding.ivHappy to "Senang", + binding.ivSad to "Sedih", + binding.ivNormal to "Normal", + binding.ivAngry to "Marah", + binding.ivFrustated to "Kecewa" + ) + + val actitityToday = resources.getStringArray(R.array.activity_today) + val aTodayAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, actitityToday) + binding.actvActivityType.setAdapter(aTodayAdapter) + + setupEmotionSelection() + setupSaveButton() + observeDiarySaveResult() + setupTextWatchers() + } + + private fun setupEmotionSelection() { + emotionMap.keys.forEach { imageView -> + imageView.setOnClickListener { + val selectedEmotion = emotionMap[imageView] ?: return@setOnClickListener + viewModel.setSelectedEmotion(selectedEmotion) + } + } + + viewModel.selectedEmotion.observe(viewLifecycleOwner) { selectedEmotion -> + emotionMap.forEach { (imageView, emotion) -> + val color = if (emotion == selectedEmotion) R.color.bluePrimary else R.color.gray400 + imageView.setColorFilter(ContextCompat.getColor(requireContext(), color)) + + if (emotion == selectedEmotion) { + // Make TextView visible for selected emotion + getEmotionTextView(emotion)?.visibility = View.VISIBLE + } else { + // Hide TextView for unselected emotions + getEmotionTextView(emotion)?.visibility = View.GONE + } + } + } + } + // Function to get TextView based on selected emotion + private fun getEmotionTextView(emotion: String): TextView? { + return when (emotion) { + "Senang" -> binding.tvHappy + "Sedih" -> binding.tvSad + "Normal" -> binding.tvNormal + "Marah" -> binding.tvAngry + "Kecewa" -> binding.tvFrustated + else -> null + } + } + + private fun setupSaveButton() { + binding.btnSimpanDiary.setOnClickListener { + val title = binding.etTitle.text.toString().trim() + val content = binding.etContent.text.toString().trim() + val activity = binding.actvActivityType.text.toString().trim() + val emotion = viewModel.selectedEmotion.value ?: "Normal" + + if (title.isEmpty() || content.isEmpty() || activity.isEmpty()) { + Toast.makeText(requireContext(), "Semua field harus diisi!", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + // Buat data dengan struktur yang benar + val diaryData = DiaryModel( + isiDiary = mapOf( + "judul" to title, + "isi" to content, + "kegiatan" to activity, + "emosi" to emotion + ), + tanggal = com.google.firebase.Timestamp.now() + ) + + viewModel.addDiary(diaryData) + } + } + + + private fun observeDiarySaveResult() { + viewModel.diarySaveResult.observe(viewLifecycleOwner) { result -> + if (result.isSuccess) { + ToastUtils.showToast(requireContext(),"Diary berhasil disimpan!", Toast.LENGTH_SHORT, position = ToastUtils.Position.TOP) + + requireActivity().onBackPressed() // Kembali ke list diary + } else { + ToastUtils.showToast(requireContext(),"Gagal menyimpan diary: ${result.exceptionOrNull()?.message}", Toast.LENGTH_SHORT, position = ToastUtils.Position.TOP) + + } + } + } + + private fun setButtonState() { + val isAllFieldFilled = binding.actvActivityType.text.toString().isNotEmpty() && binding.etTitle.text.toString().isNotEmpty() + &&binding.etContent.text.toString().isNotEmpty() + + binding.btnSimpanDiary.isEnabled = isAllFieldFilled + } + + private fun setupTextWatchers() { + binding.actvActivityType.addTextChangedListener { setButtonState() } + binding.etTitle.addTextChangedListener { setButtonState() } + binding.etContent.addTextChangedListener { setButtonState() } + } + + companion object { + + @JvmStatic + fun newInstance(param1: String, param2: String) = + CreateDiaryFragment().apply { + arguments = Bundle().apply { + + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/diary/fragments/DiaryListFragment.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/fragments/DiaryListFragment.kt new file mode 100644 index 0000000..8bd932c --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/diary/fragments/DiaryListFragment.kt @@ -0,0 +1,88 @@ +package com.example.app_skripsi.ui.profile.diary.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.app_skripsi.data.firebase.FirebaseService +import com.example.app_skripsi.data.repository.DiaryRepository +import com.example.app_skripsi.databinding.FragmentDiaryListBinding +import com.example.app_skripsi.ui.profile.diary.DiaryActivity +import com.example.app_skripsi.ui.profile.diary.adapter.DiaryAdapter +import com.example.app_skripsi.ui.profile.diary.DiaryViewModel +import com.example.app_skripsi.ui.profile.diary.DiaryViewModelFactory + + +class DiaryListFragment : Fragment() { + private var _binding: FragmentDiaryListBinding? = null + private val binding get() = _binding!! + + private val viewModel: DiaryViewModel by viewModels { + DiaryViewModelFactory(DiaryRepository(FirebaseService())) + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentDiaryListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.btnBack.setOnClickListener { + requireActivity().finish() + } + + val diaryAdapter = DiaryAdapter(emptyList()) // Buat adapter dengan list kosong + binding.recyclerViewDiary.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = diaryAdapter + } + + // 🔥 Ambil data diary dari ViewModel + viewModel.fetchDiaries() + + // 🔥 Observe perubahan data diary + viewModel.diaries.observe(viewLifecycleOwner) { diaryList -> + if (diaryList.isEmpty()) { + binding.tvEmptyMessage.visibility = View.VISIBLE // Tampilkan pesan kosong + } else { + binding.tvEmptyMessage.visibility = View.GONE + diaryAdapter.updateData(diaryList) // 🔥 Gunakan `DiffUtil` untuk update data + } + } + + binding.fabCreateDiary.setOnClickListener { + (requireActivity() as DiaryActivity).navigateToCreateDiary() + } + } + + companion object { + + @JvmStatic + fun newInstance(param1: String, param2: String) = + DiaryListFragment().apply { + arguments = Bundle().apply { + + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/faq/FaqActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/faq/FaqActivity.kt new file mode 100644 index 0000000..1f8628b --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/faq/FaqActivity.kt @@ -0,0 +1,63 @@ +package com.example.app_skripsi.ui.profile.faq + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.FaqModel +import com.example.app_skripsi.databinding.ActivityAboutBinding +import com.example.app_skripsi.databinding.ActivityFaqBinding + +class FaqActivity : AppCompatActivity() { + private var _binding : ActivityFaqBinding? = null + private val binding get() = _binding!! + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + _binding = ActivityFaqBinding.inflate(layoutInflater) + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val bottomNav = insets.getInsets(WindowInsetsCompat.Type.ime()) + v.setPadding(systemBars.left+v.paddingLeft , systemBars.top + v.paddingTop, + systemBars.right + v.paddingRight , systemBars.bottom + v.paddingBottom) + insets + } + + binding.btnBack.setOnClickListener { + finish() + } + + hideSystemUI() + setupRecyclerView() + } + + private fun setupRecyclerView() { + val faqList = listOf( + FaqModel("Apa itu GAD 7?", "GAD 7 ini adalah alat yang digunakan untuk mengukur tingkat kecemasan."), + FaqModel("Apa itu kecemasan?", "Kecemasan adalah respon alami terhadap stres yang bisa bersifat sementara atau kronis."), + FaqModel("Bagaimana cara mengatasi kecemasan?", "Mengelola kecemasan bisa dilakukan dengan teknik relaksasi, olahraga, atau terapi psikologis."), + FaqModel("Apakah aplikasi ini bisa memberikan solusi?", "Aplikasi ini menyediakan rekomendasi berbasis GAD 7 dan saran untuk mengelola kecemasan."), + FaqModel("Bagaimana cara menggunakan aplikasi ini?", "Cukup daftar, lakukan tes GAD 7, dan lihat hasil analisa kecemasan.") + ) + + binding.recyclerViewFaq.layoutManager = LinearLayoutManager(this) + binding.recyclerViewFaq.adapter = FaqAdapter(faqList) + } + + private fun hideSystemUI() { + WindowInsetsControllerCompat(window, window.decorView).apply { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/ui/profile/faq/FaqAdapter.kt b/app/src/main/java/com/example/app_skripsi/ui/profile/faq/FaqAdapter.kt new file mode 100644 index 0000000..e27c533 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/profile/faq/FaqAdapter.kt @@ -0,0 +1,45 @@ +package com.example.app_skripsi.ui.profile.faq + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.app_skripsi.R +import com.example.app_skripsi.data.model.FaqModel +import com.example.app_skripsi.databinding.ItemFaqBinding + +class FaqAdapter(private val faqList: List) : RecyclerView.Adapter() { + + class FaqViewHolder(val binding: ItemFaqBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FaqViewHolder { + val binding = ItemFaqBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return FaqViewHolder(binding) + } + + override fun onBindViewHolder(holder: FaqViewHolder, position: Int) { + val faq = faqList[position] + + holder.binding.tvTitleFaq.text = faq.question + holder.binding.tvDescFaq.text = faq.answer + + // Set visibility berdasarkan status isExpanded + holder.binding.tvDescFaq.visibility = if (faq.isExpanded) View.VISIBLE else View.GONE + holder.binding.ivSeeDetail.setImageResource(if (faq.isExpanded) R.drawable.add_2 else R.drawable.add) + + // Handle klik untuk expand/hide + holder.binding.root.setOnClickListener { + faq.isExpanded = !faq.isExpanded + notifyItemChanged(position) + } + + holder.binding.ivSeeDetail.setOnClickListener { + faq.isExpanded = !faq.isExpanded + notifyItemChanged(position) + } + } + + override fun getItemCount(): Int = faqList.size +} diff --git a/app/src/main/java/com/example/app_skripsi/ui/splashscreen/SplashScreenActivity.kt b/app/src/main/java/com/example/app_skripsi/ui/splashscreen/SplashScreenActivity.kt new file mode 100644 index 0000000..b785163 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/ui/splashscreen/SplashScreenActivity.kt @@ -0,0 +1,21 @@ +package com.example.app_skripsi.ui.splashscreen + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.example.app_skripsi.R + +class SplashScreenActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_splash_screen) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/utils/EmotionUtils.kt b/app/src/main/java/com/example/app_skripsi/utils/EmotionUtils.kt new file mode 100644 index 0000000..f422381 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/utils/EmotionUtils.kt @@ -0,0 +1,16 @@ +package com.example.app_skripsi.utils + +import com.example.app_skripsi.R + +object EmotionUtils { + fun getEmotionIcon(emotion: String): Int { + return when (emotion.trim().lowercase()) { + "senang" -> R.drawable.ic_happy + "sedih" -> R.drawable.ic_sad + "marah" -> R.drawable.ic_angry + "normal" -> R.drawable.ic_normal + "kecewa" -> R.drawable.ic_frustated + else -> R.drawable.ic_normal // Default jika tidak dikenal + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/utils/NotificationSchedulerManager.kt b/app/src/main/java/com/example/app_skripsi/utils/NotificationSchedulerManager.kt new file mode 100644 index 0000000..33d3434 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/utils/NotificationSchedulerManager.kt @@ -0,0 +1,493 @@ +package com.example.app_skripsi.utils + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.work.* +import com.example.app_skripsi.receivers.PredictionAlarmReceiver +import com.example.app_skripsi.receivers.RoutineFormAlarmReceiver +import com.example.app_skripsi.workers.DailyNotificationWorker +import com.example.app_skripsi.workers.PredictionNotificationWorker +import com.example.app_skripsi.workers.RoutineFormReminderWorker +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit + +/** + * Utility class to manage scheduling of different notification types using WorkManager. + * Provides methods to schedule and cancel notifications for anxiety predictions and + * routine form reminders. + */ +class NotificationSchedulerManager(private val context: Context) { + + companion object { + private const val TAG = "NotificationScheduler" + + // AlarmManager codes + private const val ALARM_REQUEST_CODE = 1001 + + // Work request tags + const val TAG_PREDICTION_NOTIFICATION = "prediction_notification" + const val TAG_ROUTINE_FORM_REMINDER_1 = "routine_form_reminder_1" + const val TAG_ROUTINE_FORM_REMINDER_2 = "routine_form_reminder_2" + const val TAG_ROUTINE_FORM_REMINDER_3 = "routine_form_reminder_3" + + // Work request names + const val WORK_NAME_PREDICTION = "prediction_notification_work" + const val WORK_NAME_ROUTINE_REMINDER = "routine_form_reminder_work" + + // Preference keys + const val PREF_NOTIFICATIONS_ENABLED = "prediction_notifications_enabled" + private const val ROUTINE_ALARM_BASE_CODE = 2000 + } + + /** + * Schedules daily notifications using AlarmManager for more reliable execution + */ + fun scheduleDailyPredictionNotificationWithAlarm(hour: Int = 8, minute: Int = 0): Boolean { + return try { + Log.d(TAG, "Scheduling with AlarmManager at $hour:$minute") + + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + // Create intent for AlarmReceiver + val intent = Intent(context, PredictionAlarmReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast( + context, + ALARM_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Calculate time for next alarm + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + + // If time has passed today, schedule for tomorrow + if (timeInMillis <= System.currentTimeMillis()) { + add(Calendar.DAY_OF_YEAR, 1) + } + } + + // Set exact alarm based on Android version + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } + + // Save notification enabled state + context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + .edit() + .putBoolean(PREF_NOTIFICATIONS_ENABLED, true) + .apply() + + Log.d(TAG, "AlarmManager set for: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(calendar.time)}") + true + } catch (e: Exception) { + Log.e(TAG, "Failed to schedule with AlarmManager", e) + false + } + } + + /** + * Cancels AlarmManager notifications + */ + fun cancelAlarmNotifications() { + try { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(context, PredictionAlarmReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast( + context, + ALARM_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + alarmManager.cancel(pendingIntent) + + // Save notification disabled state + context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + .edit() + .putBoolean(PREF_NOTIFICATIONS_ENABLED, false) + .apply() + + Log.d(TAG, "AlarmManager notifications cancelled") + } catch (e: Exception) { + Log.e(TAG, "Error cancelling alarm notifications", e) + } + } + + + fun isPredictionNotificationsScheduled(): Boolean { + return context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + .getBoolean(PREF_NOTIFICATIONS_ENABLED, false) + } + + + fun scheduleDailyPredictionNotification(hour: Int = 8, minute: Int = 0): Boolean { + return try { + Log.d(TAG, "========= SCHEDULING PREDICTION NOTIFICATION =========") + Log.d(TAG, "Target time: $hour:$minute") + + // Cancel any existing work first + cancelPredictionNotifications() + + // Calculate initial delay to first notification + val initialDelayMillis = calculateInitialDelay(hour, minute) + + // Untuk notifikasi pertama, gunakan OneTimeWorkRequest + if (initialDelayMillis < TimeUnit.HOURS.toMillis(1)) { + scheduleOneTimeNotification(initialDelayMillis) + } + + // Schedule periodic work untuk hari berikutnya + schedulePeriodicNotification(hour, minute) + + Log.d(TAG, "Successfully scheduled daily prediction notification") + true + } catch (e: Exception) { + Log.e(TAG, "Failed to schedule daily prediction notification", e) + false + } + } + + private fun scheduleOneTimeNotification(delayMillis: Long) { + val oneTimeRequest = OneTimeWorkRequestBuilder() + .setInitialDelay(delayMillis, TimeUnit.MILLISECONDS) + .addTag(TAG_PREDICTION_NOTIFICATION) + .build() + + WorkManager.getInstance(context) + .enqueue(oneTimeRequest) + + Log.d(TAG, "Scheduled one-time notification with delay: $delayMillis ms") + } + + private fun schedulePeriodicNotification(hour: Int, minute: Int) { + // Hitung delay untuk notifikasi besok + val tomorrowDelay = calculateDelayForTomorrow(hour, minute) + + val periodicRequest = PeriodicWorkRequestBuilder( + 24, TimeUnit.HOURS, + 15, TimeUnit.MINUTES + ) + .setInitialDelay(tomorrowDelay, TimeUnit.MILLISECONDS) + .addTag(TAG_PREDICTION_NOTIFICATION) + .build() + + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork( + WORK_NAME_PREDICTION, + ExistingPeriodicWorkPolicy.REPLACE, + periodicRequest + ) + } + + private fun calculateDelayForTomorrow(hour: Int, minute: Int): Long { + val calendar = Calendar.getInstance() + val targetTime = calendar.clone() as Calendar + + targetTime.set(Calendar.HOUR_OF_DAY, hour) + targetTime.set(Calendar.MINUTE, minute) + targetTime.set(Calendar.SECOND, 0) + targetTime.set(Calendar.MILLISECOND, 0) + + // Selalu jadwalkan untuk besok + targetTime.add(Calendar.DAY_OF_YEAR, 1) + + return targetTime.timeInMillis - calendar.timeInMillis + } + + // Tambahkan method baru di NotificationSchedulerManager + fun scheduleRoutineFormRemindersWithAlarm(userId: String): Boolean { + return try { + Log.d(TAG, "Scheduling routine form reminders with AlarmManager for user: $userId") + + // Cancel existing alarms dulu + cancelRoutineFormAlarms() + + val currentTime = Calendar.getInstance() + val currentHour = currentTime.get(Calendar.HOUR_OF_DAY) + val currentMinute = currentTime.get(Calendar.MINUTE) + + // Schedule morning reminder (9:00 AM) - hanya jika sebelum jam 9 + if (currentHour < 9 || (currentHour == 9 && currentMinute == 0)) { + scheduleRoutineFormAlarm(9, 0, 1, userId, false) + } else { + scheduleRoutineFormAlarm(9, 0, 1, userId, true) + } + + // Schedule afternoon reminder (2:00 PM) - hanya jika sebelum jam 14 + if (currentHour < 14 || (currentHour == 14 && currentMinute == 0)) { + scheduleRoutineFormAlarm(14, 0, 2, userId, false) + } else { + scheduleRoutineFormAlarm(14, 0, 2, userId, true) + } + + // Schedule evening reminder (8:00 PM) - hanya jika sebelum jam 20 + if (currentHour < 20 || (currentHour == 20 && currentMinute == 0)) { + scheduleRoutineFormAlarm(20, 0, 3, userId, false) + } else { + scheduleRoutineFormAlarm(20, 0, 3, userId, true) + } + + Log.d(TAG, "Successfully scheduled routine form reminders") + true + } catch (e: Exception) { + Log.e(TAG, "Failed to schedule routine form reminders", e) + false + } + } + + private fun scheduleRoutineFormAlarm( + hour: Int, + minute: Int, + reminderNumber: Int, + userId: String, + forTomorrow: Boolean = false + ) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + val intent = Intent(context, RoutineFormAlarmReceiver::class.java).apply { + putExtra(RoutineFormAlarmReceiver.EXTRA_REMINDER_NUMBER, reminderNumber) + putExtra(RoutineFormAlarmReceiver.EXTRA_USER_ID, userId) + } + + val requestCode = ROUTINE_ALARM_BASE_CODE + reminderNumber + val pendingIntent = PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + + // Jika waktu sudah lewat atau forTomorrow true, jadwalkan untuk besok + if (forTomorrow || timeInMillis <= System.currentTimeMillis()) { + add(Calendar.DAY_OF_YEAR, 1) + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } + + val scheduledTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(calendar.timeInMillis)) + Log.d(TAG, "Scheduled routine form reminder #$reminderNumber for $scheduledTime") + } + + fun cancelRoutineFormAlarms() { + try { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + for (i in 1..3) { + val intent = Intent(context, RoutineFormAlarmReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast( + context, + ROUTINE_ALARM_BASE_CODE + i, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + alarmManager.cancel(pendingIntent) + } + + Log.d(TAG, "Cancelled all routine form alarms") + } catch (e: Exception) { + Log.e(TAG, "Error cancelling routine form alarms", e) + } + } + + private fun scheduleRoutineFormReminder( + hour: Int, + minute: Int, + reminderNumber: Int, + tag: String + ) { + val initialDelayMillis = calculateInitialDelay(hour, minute) + + // Data for the worker + val inputData = Data.Builder() + .putInt(RoutineFormReminderWorker.KEY_REMINDER_NUMBER, reminderNumber) + .build() + + // Create work request + val reminderConstraints = Constraints.Builder() + .build() + + val reminderWorkRequest = PeriodicWorkRequestBuilder( + 24, TimeUnit.HOURS // Repeat every 24 hours + ) + .setConstraints(reminderConstraints) + .setInitialDelay(initialDelayMillis, TimeUnit.MILLISECONDS) + .setInputData(inputData) + .addTag(tag) + .build() + + // Enqueue the work request with unique name based on reminder number + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork( + "$WORK_NAME_ROUTINE_REMINDER$reminderNumber", + ExistingPeriodicWorkPolicy.REPLACE, + reminderWorkRequest + ) + + Log.d(TAG, "Scheduled routine form reminder #$reminderNumber at $hour:$minute") + } + + /** + * Cancels all prediction notifications + */ + fun cancelPredictionNotifications() { + try { + WorkManager.getInstance(context) + .cancelUniqueWork(WORK_NAME_PREDICTION) + + Log.d(TAG, "Cancelled all prediction notifications") + } catch (e: Exception) { + Log.e(TAG, "Error cancelling prediction notifications", e) + } + } + + fun cancelRoutineFormReminders() { + try { + // Cancel all three reminders + for (i in 1..3) { + WorkManager.getInstance(context) + .cancelUniqueWork("$WORK_NAME_ROUTINE_REMINDER$i") + } + + Log.d(TAG, "Cancelled all routine form reminders") + } catch (e: Exception) { + Log.e(TAG, "Error cancelling routine form reminders", e) + } + } + + private fun calculateInitialDelay(hour: Int, minute: Int): Long { + val calendar = Calendar.getInstance() + val targetTime = calendar.clone() as Calendar + + targetTime.set(Calendar.HOUR_OF_DAY, hour) + targetTime.set(Calendar.MINUTE, minute) + targetTime.set(Calendar.SECOND, 0) + targetTime.set(Calendar.MILLISECOND, 0) + + Log.d(TAG, "Current calendar time: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(calendar.time)}") + Log.d(TAG, "Target calendar time: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(targetTime.time)}") + + // Berikan grace period 5 menit + val currentTimeMillis = calendar.timeInMillis + val targetTimeMillis = targetTime.timeInMillis + + // Jika waktu target sudah lewat lebih dari 5 menit, jadwalkan untuk besok + if (targetTimeMillis < currentTimeMillis - (5 * 60 * 1000)) { + targetTime.add(Calendar.DAY_OF_YEAR, 1) + Log.d(TAG, "Target time is in the past (more than 5 minutes), scheduling for tomorrow") + } else if (targetTimeMillis < currentTimeMillis) { + // Jika baru lewat kurang dari 5 menit, jadwalkan segera + Log.d(TAG, "Target time just passed (less than 5 minutes), scheduling immediately") + return 1000 // 1 detik delay + } + + val delay = targetTime.timeInMillis - calendar.timeInMillis + Log.d(TAG, "Calculated delay: $delay ms") + + return delay + } + + fun scheduleNextRoutineReminder(reminderNumber: Int, userId: String) { + try { + Log.d(TAG, "Scheduling next routine reminder #$reminderNumber for user: $userId") + + // Tentukan waktu untuk alarm berikutnya berdasarkan reminder number + val (hour, minute) = when (reminderNumber) { + 1 -> Pair(9, 0) // Pagi + 2 -> Pair(14, 0) // Siang + 3 -> Pair(20, 0) // Malam + else -> Pair(9, 0) // Default ke pagi + } + + // Jadwalkan untuk besok + scheduleRoutineFormAlarmForTomorrow(hour, minute, reminderNumber, userId) + + } catch (e: Exception) { + Log.e(TAG, "Error scheduling next routine reminder", e) + } + } + + private fun scheduleRoutineFormAlarmForTomorrow(hour: Int, minute: Int, reminderNumber: Int, userId: String) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + val intent = Intent(context, RoutineFormAlarmReceiver::class.java).apply { + putExtra(RoutineFormAlarmReceiver.EXTRA_REMINDER_NUMBER, reminderNumber) + putExtra(RoutineFormAlarmReceiver.EXTRA_USER_ID, userId) + } + + val requestCode = ROUTINE_ALARM_BASE_CODE + reminderNumber + val pendingIntent = PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Atur waktu untuk besok + val calendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, hour) + set(Calendar.MINUTE, minute) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + + // Tambahkan 1 hari + add(Calendar.DAY_OF_YEAR, 1) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) + } + + Log.d(TAG, "Scheduled next routine reminder #$reminderNumber for tomorrow at $hour:$minute") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/utils/PredictionVisualizationHelper.kt b/app/src/main/java/com/example/app_skripsi/utils/PredictionVisualizationHelper.kt new file mode 100644 index 0000000..5bb178c --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/utils/PredictionVisualizationHelper.kt @@ -0,0 +1,4 @@ +package com.example.app_skripsi.utils + +class PredictionVisualizationHelper { +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/utils/ToastUtils.kt b/app/src/main/java/com/example/app_skripsi/utils/ToastUtils.kt new file mode 100644 index 0000000..47c327f --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/utils/ToastUtils.kt @@ -0,0 +1,31 @@ +package com.example.app_skripsi.utils + +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import android.widget.Toast +import com.example.app_skripsi.R + + object ToastUtils { + enum class Position { TOP, BOTTOM } + + fun showToast(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT, position: Position = Position.BOTTOM) { + val inflater = LayoutInflater.from(context) + val layout: View = inflater.inflate(R.layout.custom_toast, null) + + val text: TextView = layout.findViewById(R.id.toast_text) + text.text = message + + val toast = Toast(context) + toast.duration = duration + toast.view = layout + toast.setGravity( + if (position == Position.TOP) Gravity.TOP or Gravity.FILL_HORIZONTAL else Gravity.BOTTOM or Gravity.FILL_HORIZONTAL, + 0, + 100 + ) + toast.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/workers/DailyNotificationWorker.kt b/app/src/main/java/com/example/app_skripsi/workers/DailyNotificationWorker.kt new file mode 100644 index 0000000..1958098 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/workers/DailyNotificationWorker.kt @@ -0,0 +1,62 @@ +package com.example.app_skripsi.workers + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.example.app_skripsi.MainActivity +import com.example.app_skripsi.R + +class DailyNotificationWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + + companion object { + const val CHANNEL_ID = "DAILY_NOTIFICATION_CHANNEL" + const val NOTIFICATION_ID = 1 + } + + override fun doWork(): Result { + // Create notification + createNotificationChannel() + + val intent = Intent(applicationContext, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setContentTitle("Reminder") + .setContentText("Ini adalah pengingat rutin Anda!") + .setSmallIcon(R.drawable.ic_about) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .build() + + // Show notification + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(NOTIFICATION_ID, notification) + + // Return success + return Result.success() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val name = "Daily Notifications" + val descriptionText = "Channel for daily reminders" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + enableLights(true) + enableVibration(true) + vibrationPattern = longArrayOf(0, 200, 100, 200) + } + val notificationManager = applicationContext.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/workers/PeriodicNotificationWorker.kt b/app/src/main/java/com/example/app_skripsi/workers/PeriodicNotificationWorker.kt new file mode 100644 index 0000000..b038f87 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/workers/PeriodicNotificationWorker.kt @@ -0,0 +1,62 @@ +package com.example.app_skripsi.workers + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.example.app_skripsi.MainActivity +import com.example.app_skripsi.R + +class PeriodicNotificationWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + + companion object { + const val CHANNEL_ID = "PERIODIC_NOTIFICATION_CHANNEL" + const val NOTIFICATION_ID = 2 + } + + override fun doWork(): Result { + // Membuat notifikasi + createNotificationChannel() + + val intent = Intent(applicationContext, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setContentTitle("Periodic Reminder") + .setContentText("Ini adalah notifikasi periodik setiap 2 jam!") + .setSmallIcon(R.drawable.ic_about) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .build() + + // Menampilkan notifikasi + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(NOTIFICATION_ID, notification) + + // Mengembalikan status sukses + return Result.success() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val name = "Periodic Notifications" + val descriptionText = "Channel for periodic reminders every 2 hours" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + enableLights(true) + enableVibration(true) + vibrationPattern = longArrayOf(0, 200, 100, 200) + } + val notificationManager = applicationContext.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/workers/PredictionNotificationWorker.kt b/app/src/main/java/com/example/app_skripsi/workers/PredictionNotificationWorker.kt new file mode 100644 index 0000000..cb00d22 --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/workers/PredictionNotificationWorker.kt @@ -0,0 +1,242 @@ +package com.example.app_skripsi.workers + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.example.app_skripsi.MainActivity +import com.example.app_skripsi.R +import com.example.app_skripsi.core.knn.PersonalPredictionService +import com.example.app_skripsi.data.firebase.FirebaseService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +/** + * Worker that sends notifications with anxiety predictions based on user's personal data. + * This notification includes prediction for the current day, showing expected anxiety level, + * emotions, activities, and recommendations. + */ +class PredictionNotificationWorker( + private val appContext: Context, + private val workerParams: WorkerParameters +) : CoroutineWorker(appContext, workerParams) { + + companion object { + const val CHANNEL_ID = "PREDICTION_NOTIFICATION_CHANNEL" + const val NOTIFICATION_ID = 3 + const val TAG = "PredictionNotifWorker" + + // Extra data keys for input + const val KEY_DOCUMENT_ID = "documentId" + } + + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + try { + + + Log.d(TAG, "========= PREDICTION NOTIFICATION WORKER STARTED =========") + Log.d(TAG, "Worker executed at: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())}") + Log.d(TAG, "Worker ID: ${workerParams.id}") + // Check if notification was already shown today to prevent duplicates + val prefs = appContext.getSharedPreferences("notification_prefs", Context.MODE_PRIVATE) + val lastNotificationDate = prefs.getString("last_prediction_notification", "") + val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + + if (lastNotificationDate == today) { + Log.d(TAG, "Notification already shown today, skipping") + return@withContext Result.success() + } + + // Create notification channel first + createNotificationChannel() + + // Get current day of week + val calendar = Calendar.getInstance() + val currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1 + val currentDayName = getDayName(currentDayOfWeek) + + Log.d(TAG, "Current day: $currentDayOfWeek ($currentDayName)") + + // Get prediction for today + val prediction = getTodayPrediction(currentDayOfWeek) + if (prediction == null) { + Log.e(TAG, "No prediction available for today") + return@withContext Result.failure() + } + + Log.d(TAG, "Prediction found: ${prediction.predictedSeverity}") + + // Create notification + val notificationText = buildNotificationText(prediction) + + // Show notification + showNotification(notificationText) + + // Save the date after showing notification successfully + prefs.edit().putString("last_prediction_notification", today).apply() + + Log.d(TAG, "========= PREDICTION NOTIFICATION WORKER COMPLETED =========") + Result.success() + } catch (e: Exception) { + Log.e(TAG, "Error in prediction notification worker", e) + e.printStackTrace() + Result.failure() + } + } + + private fun showNotification(text: String) { + val intent = Intent(appContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + val pendingIntent = PendingIntent.getActivity( + appContext, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(appContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_about) + .setContentTitle("Prediksi Kecemasan Hari Ini") + .setContentText(text) + .setStyle(NotificationCompat.BigTextStyle().bigText(text)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .build() + + val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(NOTIFICATION_ID, notification) + + Log.d(TAG, "Notification posted with ID: $NOTIFICATION_ID") + } + + /** + * Creates formatted notification text from prediction details + */ + private fun buildNotificationText(prediction: PersonalPredictionService.PredictionDetail): String { + // Format based on anxiety level + val severityText = when (prediction.predictedSeverity) { + "Minimal" -> "tingkat kecemasan minimal" + "Ringan" -> "tingkat kecemasan ringan" + "Sedang" -> "tingkat kecemasan sedang" + "Parah" -> "tingkat kecemasan parah" + else -> "tingkat kecemasan yang tidak dapat diprediksi" + } + + return "Hari ini Anda diperkirakan akan mengalami $severityText " + + "dengan emosi ${prediction.suggestedEmosi} saat melakukan aktivitas ${prediction.suggestedAktivitas}.\n\n" + + "Rekomendasi: ${prediction.getRecommendedActivity()}" + } + + /** + * Gets prediction for today from user's personal data + */ + private suspend fun getTodayPrediction(currentDayOfWeek: Int): PersonalPredictionService.PredictionDetail? { + try { + val firebaseService = FirebaseService() + val userId = firebaseService.getCurrentUserId() ?: return null + + // Get latest routine detection data + val result = firebaseService.getRoutineDetections(userId) + if (result.isFailure) { + Log.e(TAG, "Failed to get routine detections: ${result.exceptionOrNull()?.message}") + return null + } + + val detections = result.getOrNull() + if (detections.isNullOrEmpty()) { + Log.d(TAG, "No detection data available") + return null + } + + // Get latest completed detection + val latestDetection = detections + .filter { !it.second.aktif } + .maxByOrNull { it.first.substringAfterLast("_").toIntOrNull() ?: 0 } + ?.second + + if (latestDetection == null) { + Log.d(TAG, "No completed detection found") + return null + } + + // Convert daily data to list + val dailyDataList = latestDetection.deteksiHarian.values.toList().sortedBy { it.tanggal } + + // Get last day number + val lastDayNumber = latestDetection.deteksiHarian.keys + .mapNotNull { it.toIntOrNull() } + .maxOrNull() ?: 0 + + // Create prediction service and get predictions + val predictionService = PersonalPredictionService(appContext) + val predictions = predictionService.predictNextSevenDaysDetail(lastDayNumber, dailyDataList) + + if (predictions.isEmpty()) { + Log.d(TAG, "No predictions generated") + return null + } + + // Find prediction for current day of week + return predictions.find { getDayOfWeek(it.dayOfWeek) == currentDayOfWeek } + } catch (e: Exception) { + Log.e(TAG, "Error retrieving prediction", e) + return null + } + } + + /** + * Creates notification channel for Android O and above + */ + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Prediksi Kecemasan" + val descriptionText = "Notifikasi untuk prediksi tingkat kecemasan harian" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + enableLights(true) + enableVibration(true) + } + + val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + /** + * Gets day name from day of week index (0-6) + */ + private fun getDayName(dayOfWeek: Int): String { + val days = arrayOf("Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu") + return days[dayOfWeek % 7] + } + + /** + * Gets day of week index (0-6) from day name + */ + private fun getDayOfWeek(dayName: String): Int { + return when (dayName.lowercase(Locale.getDefault())) { + "minggu" -> 0 + "senin" -> 1 + "selasa" -> 2 + "rabu" -> 3 + "kamis" -> 4 + "jumat" -> 5 + "sabtu" -> 6 + else -> -1 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/app_skripsi/workers/RoutineFormReminderWorker.kt b/app/src/main/java/com/example/app_skripsi/workers/RoutineFormReminderWorker.kt new file mode 100644 index 0000000..0d1a4ce --- /dev/null +++ b/app/src/main/java/com/example/app_skripsi/workers/RoutineFormReminderWorker.kt @@ -0,0 +1,160 @@ +package com.example.app_skripsi.workers + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.example.app_skripsi.MainActivity +import com.example.app_skripsi.R +import com.example.app_skripsi.data.local.RoutineSessionManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Worker that sends reminders to fill out routine anxiety detection forms + * when there is an active session and the user hasn't completed the form for today. + * + * This worker can be scheduled to run up to 3 times a day at different times + * to ensure the user doesn't miss filling out the form. + */ +class RoutineFormReminderWorker( + private val appContext: Context, + private val workerParams: WorkerParameters +) : CoroutineWorker(appContext, workerParams) { + + companion object { + const val CHANNEL_ID = "ROUTINE_FORM_REMINDER_CHANNEL" + const val NOTIFICATION_ID_BASE = 100 // Use different ID for each notification time + const val TAG = "RoutineFormReminder" + + // Input data keys + const val KEY_REMINDER_NUMBER = "reminderNumber" // 1, 2, or 3 for different times of day + } + + // Get the routine session manager to check session status + private val routineSessionManager = RoutineSessionManager(appContext) + + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + try { + Log.d(TAG, "Starting routine form reminder check") + + // Check if there's an active session + val isSessionActive = routineSessionManager.isSessionStillActive() + if (!isSessionActive) { + Log.d(TAG, "No active routine session found") + return@withContext Result.success() // Nothing to do if no active session + } + + // Check if user already completed form for today + val completedToday = routineSessionManager.hasCompletedFormToday() + if (completedToday) { + Log.d(TAG, "User already completed form today") + return@withContext Result.success() // Nothing to do if form already completed + } + + // Get reminder number to customize message and notification ID + val reminderNumber = workerParams.inputData.getInt(KEY_REMINDER_NUMBER, 1) + val notificationId = NOTIFICATION_ID_BASE + reminderNumber + + // Create notification channel + createNotificationChannel() + + // Get session details for more personalized reminder + val sessionType = routineSessionManager.getSessionTypeDisplay() + val currentDay = routineSessionManager.getCurrentSessionDay() + val totalDays = routineSessionManager.getSessionDurationInDays() + + // Create intent to open MainActivity (you might want to deep link to form activity) + val intent = Intent(appContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + // You can add extras here to take user directly to form + putExtra("OPEN_FORM", true) + putExtra("DETECTION_TYPE", "ROUTINE") + } + + val pendingIntent = PendingIntent.getActivity( + appContext, + notificationId, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + // Create notification with reminder + val notificationTitle = "Pengingat Deteksi Kecemasan" + val notificationText = buildReminderText(reminderNumber, sessionType, currentDay, totalDays) + + val notification = NotificationCompat.Builder(appContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_about) // Use appropriate icon + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setStyle(NotificationCompat.BigTextStyle().bigText(notificationText)) + .setPriority(NotificationCompat.PRIORITY_HIGH) // Higher priority for reminders + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .build() + + // Show the notification + val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(notificationId, notification) + + Log.d(TAG, "Routine form reminder #$reminderNumber shown successfully") + Result.success() + } catch (e: Exception) { + Log.e(TAG, "Error showing routine form reminder", e) + Result.failure() + } + } + + /** + * Builds the reminder text based on reminder number and session details + */ + private fun buildReminderText( + reminderNumber: Int, + sessionType: String, + currentDay: Int, + totalDays: Int + ): String { + val dayFormat = SimpleDateFormat("EEEE", Locale("id", "ID")) + val today = dayFormat.format(Date()) + + return when (reminderNumber) { + 1 -> "Anda belum mengisi form deteksi kecemasan hari ini ($today). " + + "Ini adalah hari ke-$currentDay dari $totalDays dalam sesi $sessionType Anda." + + 2 -> "Jangan lupa mengisi form deteksi kecemasan hari ini. " + + "Pengisian rutin membantu analisis kecemasan menjadi lebih akurat." + + else -> "Pengingat terakhir! Anda belum mengisi form deteksi kecemasan hari ini. " + + "Mohon segera isi untuk kelengkapan data sesi $sessionType Anda." + } + } + + /** + * Creates notification channel for Android O and above + */ + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Pengingat Form Deteksi" + val descriptionText = "Notifikasi pengingat untuk mengisi form deteksi kecemasan rutin" + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + enableLights(true) + enableVibration(true) + } + + val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..e2a8f6c --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..f2d4e22 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..185e3d3 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..4d93e91 --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/color/bottom_nav_item_color_selector.xml b/app/src/main/res/color/bottom_nav_item_color_selector.xml new file mode 100644 index 0000000..33cb086 --- /dev/null +++ b/app/src/main/res/color/bottom_nav_item_color_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 0000000..a46f042 --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/add_2.xml b/app/src/main/res/drawable/add_2.xml new file mode 100644 index 0000000..6f2a871 --- /dev/null +++ b/app/src/main/res/drawable/add_2.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/bg_activity_type.xml b/app/src/main/res/drawable/bg_activity_type.xml new file mode 100644 index 0000000..0152eb0 --- /dev/null +++ b/app/src/main/res/drawable/bg_activity_type.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_icon_activity_form.xml b/app/src/main/res/drawable/bg_icon_activity_form.xml new file mode 100644 index 0000000..9b73e40 --- /dev/null +++ b/app/src/main/res/drawable/bg_icon_activity_form.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_icon_activity_form_inactive.xml b/app/src/main/res/drawable/bg_icon_activity_form_inactive.xml new file mode 100644 index 0000000..d433728 --- /dev/null +++ b/app/src/main/res/drawable/bg_icon_activity_form_inactive.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_top_bar.xml b/app/src/main/res/drawable/bg_top_bar.xml new file mode 100644 index 0000000..525e030 --- /dev/null +++ b/app/src/main/res/drawable/bg_top_bar.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_nav_item_background_selector.xml b/app/src/main/res/drawable/bottom_nav_item_background_selector.xml new file mode 100644 index 0000000..07e7067 --- /dev/null +++ b/app/src/main/res/drawable/bottom_nav_item_background_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_bg.xml b/app/src/main/res/drawable/circle_bg.xml new file mode 100644 index 0000000..d9fc0b5 --- /dev/null +++ b/app/src/main/res/drawable/circle_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/custom_button_background.xml b/app/src/main/res/drawable/custom_button_background.xml new file mode 100644 index 0000000..3c0b3fe --- /dev/null +++ b/app/src/main/res/drawable/custom_button_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/custom_textfield_background.xml b/app/src/main/res/drawable/custom_textfield_background.xml new file mode 100644 index 0000000..3f5c8a0 --- /dev/null +++ b/app/src/main/res/drawable/custom_textfield_background.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/hide.xml b/app/src/main/res/drawable/hide.xml new file mode 100644 index 0000000..2acfcf4 --- /dev/null +++ b/app/src/main/res/drawable/hide.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/home.xml b/app/src/main/res/drawable/home.xml new file mode 100644 index 0000000..3317edb --- /dev/null +++ b/app/src/main/res/drawable/home.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/homeunfill.xml b/app/src/main/res/drawable/homeunfill.xml new file mode 100644 index 0000000..6d473f5 --- /dev/null +++ b/app/src/main/res/drawable/homeunfill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml new file mode 100644 index 0000000..2bb351e --- /dev/null +++ b/app/src/main/res/drawable/ic_about.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_angry.xml b/app/src/main/res/drawable/ic_angry.xml new file mode 100644 index 0000000..cb75538 --- /dev/null +++ b/app/src/main/res/drawable/ic_angry.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..aedcb90 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_diary.xml b/app/src/main/res/drawable/ic_diary.xml new file mode 100644 index 0000000..3f8666d --- /dev/null +++ b/app/src/main/res/drawable/ic_diary.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_entertainment.xml b/app/src/main/res/drawable/ic_entertainment.xml new file mode 100644 index 0000000..67d0790 --- /dev/null +++ b/app/src/main/res/drawable/ic_entertainment.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_faq.xml b/app/src/main/res/drawable/ic_faq.xml new file mode 100644 index 0000000..920d377 --- /dev/null +++ b/app/src/main/res/drawable/ic_faq.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_frustated.xml b/app/src/main/res/drawable/ic_frustated.xml new file mode 100644 index 0000000..9afbeff --- /dev/null +++ b/app/src/main/res/drawable/ic_frustated.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_happy.xml b/app/src/main/res/drawable/ic_happy.xml new file mode 100644 index 0000000..7c35340 --- /dev/null +++ b/app/src/main/res/drawable/ic_happy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..93dba86 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..7185edd --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_normal.xml b/app/src/main/res/drawable/ic_normal.xml new file mode 100644 index 0000000..3dbc401 --- /dev/null +++ b/app/src/main/res/drawable/ic_normal.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 0000000..ba151cc --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_off.xml b/app/src/main/res/drawable/ic_notifications_off.xml new file mode 100644 index 0000000..cc44ac9 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_off.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_sad.xml b/app/src/main/res/drawable/ic_sad.xml new file mode 100644 index 0000000..88ef9e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_sad.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sleeping.xml b/app/src/main/res/drawable/ic_sleeping.xml new file mode 100644 index 0000000..f9e2eb0 --- /dev/null +++ b/app/src/main/res/drawable/ic_sleeping.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_social.xml b/app/src/main/res/drawable/ic_social.xml new file mode 100644 index 0000000..9ecb717 --- /dev/null +++ b/app/src/main/res/drawable/ic_social.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sport.xml b/app/src/main/res/drawable/ic_sport.xml new file mode 100644 index 0000000..8ba61e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_sport.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_videos.xml b/app/src/main/res/drawable/ic_videos.xml new file mode 100644 index 0000000..c085cd1 --- /dev/null +++ b/app/src/main/res/drawable/ic_videos.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_working.xml b/app/src/main/res/drawable/ic_working.xml new file mode 100644 index 0000000..256c116 --- /dev/null +++ b/app/src/main/res/drawable/ic_working.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/profile.xml b/app/src/main/res/drawable/profile.xml new file mode 100644 index 0000000..ed821a0 --- /dev/null +++ b/app/src/main/res/drawable/profile.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/profileunfill.xml b/app/src/main/res/drawable/profileunfill.xml new file mode 100644 index 0000000..155eefc --- /dev/null +++ b/app/src/main/res/drawable/profileunfill.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/round_indicator_active.xml b/app/src/main/res/drawable/round_indicator_active.xml new file mode 100644 index 0000000..71651ee --- /dev/null +++ b/app/src/main/res/drawable/round_indicator_active.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_indicator_inactive.xml b/app/src/main/res/drawable/round_indicator_inactive.xml new file mode 100644 index 0000000..c3928f3 --- /dev/null +++ b/app/src/main/res/drawable/round_indicator_inactive.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/scan.xml b/app/src/main/res/drawable/scan.xml new file mode 100644 index 0000000..5172efb --- /dev/null +++ b/app/src/main/res/drawable/scan.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/scanunfill.xml b/app/src/main/res/drawable/scanunfill.xml new file mode 100644 index 0000000..21ac3a3 --- /dev/null +++ b/app/src/main/res/drawable/scanunfill.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/selector_button_background.xml b/app/src/main/res/drawable/selector_button_background.xml new file mode 100644 index 0000000..d28941f --- /dev/null +++ b/app/src/main/res/drawable/selector_button_background.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/show.xml b/app/src/main/res/drawable/show.xml new file mode 100644 index 0000000..2406357 --- /dev/null +++ b/app/src/main/res/drawable/show.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/textfield_outline_background.xml b/app/src/main/res/drawable/textfield_outline_background.xml new file mode 100644 index 0000000..106cf56 --- /dev/null +++ b/app/src/main/res/drawable/textfield_outline_background.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/toast_background.xml b/app/src/main/res/drawable/toast_background.xml new file mode 100644 index 0000000..c0d3da9 --- /dev/null +++ b/app/src/main/res/drawable/toast_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/video_thumbnail.png b/app/src/main/res/drawable/video_thumbnail.png new file mode 100644 index 0000000..b05d45c Binary files /dev/null and b/app/src/main/res/drawable/video_thumbnail.png differ diff --git a/app/src/main/res/font/inter_black.ttf b/app/src/main/res/font/inter_black.ttf new file mode 100644 index 0000000..b27822b Binary files /dev/null and b/app/src/main/res/font/inter_black.ttf differ diff --git a/app/src/main/res/font/inter_bold.ttf b/app/src/main/res/font/inter_bold.ttf new file mode 100644 index 0000000..fe23eeb Binary files /dev/null and b/app/src/main/res/font/inter_bold.ttf differ diff --git a/app/src/main/res/font/inter_extra_bold.ttf b/app/src/main/res/font/inter_extra_bold.ttf new file mode 100644 index 0000000..874b1b0 Binary files /dev/null and b/app/src/main/res/font/inter_extra_bold.ttf differ diff --git a/app/src/main/res/font/inter_extra_light.ttf b/app/src/main/res/font/inter_extra_light.ttf new file mode 100644 index 0000000..c993e82 Binary files /dev/null and b/app/src/main/res/font/inter_extra_light.ttf differ diff --git a/app/src/main/res/font/inter_light.ttf b/app/src/main/res/font/inter_light.ttf new file mode 100644 index 0000000..71188f5 Binary files /dev/null and b/app/src/main/res/font/inter_light.ttf differ diff --git a/app/src/main/res/font/inter_medium.ttf b/app/src/main/res/font/inter_medium.ttf new file mode 100644 index 0000000..a01f377 Binary files /dev/null and b/app/src/main/res/font/inter_medium.ttf differ diff --git a/app/src/main/res/font/inter_regular.ttf b/app/src/main/res/font/inter_regular.ttf new file mode 100644 index 0000000..5e4851f Binary files /dev/null and b/app/src/main/res/font/inter_regular.ttf differ diff --git a/app/src/main/res/font/inter_semi_bold.ttf b/app/src/main/res/font/inter_semi_bold.ttf new file mode 100644 index 0000000..ecc7041 Binary files /dev/null and b/app/src/main/res/font/inter_semi_bold.ttf differ diff --git a/app/src/main/res/font/inter_thin.ttf b/app/src/main/res/font/inter_thin.ttf new file mode 100644 index 0000000..fe77243 Binary files /dev/null and b/app/src/main/res/font/inter_thin.ttf differ diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..0f6e13a --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dashboard.xml b/app/src/main/res/layout/activity_dashboard.xml new file mode 100644 index 0000000..f21a2ac --- /dev/null +++ b/app/src/main/res/layout/activity_dashboard.xml @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_detail_video.xml b/app/src/main/res/layout/activity_detail_video.xml new file mode 100644 index 0000000..e7bef81 --- /dev/null +++ b/app/src/main/res/layout/activity_detail_video.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_diary.xml b/app/src/main/res/layout/activity_diary.xml new file mode 100644 index 0000000..51716e1 --- /dev/null +++ b/app/src/main/res/layout/activity_diary.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_faq.xml b/app/src/main/res/layout/activity_faq.xml new file mode 100644 index 0000000..307cbce --- /dev/null +++ b/app/src/main/res/layout/activity_faq.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_forgot_pw.xml b/app/src/main/res/layout/activity_forgot_pw.xml new file mode 100644 index 0000000..2cc29e6 --- /dev/null +++ b/app/src/main/res/layout/activity_forgot_pw.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_form_anxiety.xml b/app/src/main/res/layout/activity_form_anxiety.xml new file mode 100644 index 0000000..a42ea29 --- /dev/null +++ b/app/src/main/res/layout/activity_form_anxiety.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_hasil_anxiety_short.xml b/app/src/main/res/layout/activity_hasil_anxiety_short.xml new file mode 100644 index 0000000..f1eb354 --- /dev/null +++ b/app/src/main/res/layout/activity_hasil_anxiety_short.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..981ed6a --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +