initialize commit

This commit is contained in:
Afdholanabil 2025-06-20 11:51:16 +07:00
commit e936b525df
237 changed files with 22487 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -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

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
App_Skripsi

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

7
.idea/discord.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

20
.idea/gradle.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$PROJECT_DIR$/../../../../Gradle/gradle-8.6" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.0" />
</component>
</project>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

714
.idea/other.xml Normal file
View File

@ -0,0 +1,714 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Sony" />
<option name="codename" value="A402SO" />
<option name="id" value="A402SO" />
<option name="manufacturer" value="Sony" />
<option name="name" value="Xperia 10" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2520" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP535DL1" />
<option name="id" value="OP535DL1" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2409" />
<option name="screenDensity" value="401" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15" />
<option name="id" value="a15" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15x" />
<option name="id" value="a15x" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a16x" />
<option name="id" value="a16x" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A16 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a35x" />
<option name="id" value="a35x" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A35" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="arcfox" />
<option name="id" value="arcfox" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="razr plus 2024" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="b6q" />
<option name="id" value="b6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Flip 6" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1080" />
<option name="screenY" value="2640" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm2q" />
<option name="id" value="dm2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="S23 Plus" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="dubai" />
<option name="id" value="dubai" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="edge 30" />
<option name="screenDensity" value="405" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e3q" />
<option name="id" value="e3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24 Ultra" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="eos" />
<option name="id" value="eos" />
<option name="manufacturer" value="Google" />
<option name="name" value="Eos" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="fogona" />
<option name="id" value="fogona" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2024" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="fogos" />
<option name="id" value="fogos" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g34 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="g0q" />
<option name="id" value="g0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S906U1" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7lwifi" />
<option name="id" value="gts7lwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T870" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="id" value="gts8wifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9wifi" />
<option name="id" value="gts9wifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X710" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="manaus" />
<option name="id" value="manaus" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="edge 40 neo" />
<option name="screenDensity" value="400" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="maui" />
<option name="id" value="maui" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2023" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="samsung" />
<option name="codename" value="pa3q" />
<option name="id" value="pa3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S25 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="r11q" />
<option name="id" value="r11q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S711U" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="t2q" />
<option name="id" value="t2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21 Plus" />
<option name="screenDensity" value="394" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tegu" />
<option name="id" value="tegu" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="xcover7" />
<option name="id" value="xcover7" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-G556B" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2408" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -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)

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

99
app/build.gradle.kts Normal file
View File

@ -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)
}

30
app/google-services.json Normal file
View File

@ -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"
}

25
app/proguard-rules.pro vendored Normal file
View File

@ -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 <init>();
#}

View File

@ -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)
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Add these permissions for notifications -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Add these permissions -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.App_Skripsi"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31" >
<activity
android:name=".ui.checkanxiety.RoutineResultActivity"
android:exported="false" />
<activity
android:name=".ui.checkanxiety.HasilAnxietyShortActivity"
android:exported="false"
android:launchMode="singleTop" />
<activity
android:name=".ui.checkanxiety.FormAnxietyActivity"
android:exported="false" />
<activity
android:name=".ui.detailvideo.DetailVideoActivity"
android:exported="false" />
<activity
android:name=".ui.profile.AboutActivity"
android:exported="false" />
<activity
android:name=".ui.profile.faq.FaqActivity"
android:exported="false" />
<activity
android:name=".ui.profile.RiwayatCheckActivity"
android:exported="false" />
<activity
android:name=".ui.profile.VideoListActivity"
android:exported="false" />
<activity
android:name=".ui.profile.diary.DiaryActivity"
android:exported="false" />
<activity
android:name=".ui.dashboard.DashboardActivity"
android:exported="false" />
<activity
android:name=".ui.auth.forgotPw.ForgotPwActivity"
android:exported="false" />
<activity
android:name=".ui.splashscreen.SplashScreenActivity"
android:exported="false" />
<activity
android:name=".ui.auth.register.RegisterActivity"
android:exported="false"
android:windowSoftInputMode="adjustPan" />
<activity
android:name=".ui.auth.login.LoginActivity"
android:exported="true" />
<activity
android:name=".MainActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Add worker configuration -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="shortService"
tools:node="merge" />
<!-- Boot receiver to reschedule notifications after device restart -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<receiver
android:name=".receivers.PredictionAlarmReceiver"
android:exported="false" />
<receiver
android:name=".receivers.BootCompletedReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<!-- Tambahkan receivers -->
<receiver
android:name=".receivers.RoutineFormAlarmReceiver"
android:exported="false" />
</application>
</manifest>

File diff suppressed because it is too large Load Diff

View File

@ -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()
}
}
}
}
}
}

View File

@ -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
)

View File

@ -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<DailyDetectionData>,
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<DailyDetectionData>): List<DataPoint> {
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<DataPoint> {
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<DailyDetectionData>,
predictionMode: Int = MODE_HYBRID
): List<PredictionDetail> {
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<PredictionDetail>()
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<String, Any>
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<DailyDetectionData>,
predictionMode: Int = MODE_HYBRID
): List<Pair<Int, String>> {
return predictNextSevenDaysDetail(lastDayNumber, detectionHistory, predictionMode)
.map { Pair(it.day, it.predictedSeverity) }
}
/**
* Calculates trend in GAD scores from history data
*/
private fun calculateGadScoreTrend(history: List<DailyDetectionData>): 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<DailyDetectionData>,
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()
}
}

View File

@ -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<DataPoint>,
private val k: Int = 5,
private val weights: List<Double> = 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<Pair<Double, Double>> = calculateFeatureRanges()
private fun calculateFeatureRanges(): List<Pair<Double, Double>> {
// 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<Int>): List<Double> {
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<Double>, b: List<Double>): 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<DataPoint>): 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<String, Double> {
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<String, Any> {
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<DataPoint>): Map<String, Any> {
// 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
}
}
}

View File

@ -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<DataPoint> {
try {
val inputStream = context.assets.open(TRAINING_DATA_FILENAME)
val json = inputStream.bufferedReader().use { it.readText() }
val type = object : TypeToken<List<DataPoint>>() {}.type
val data = Gson().fromJson<List<DataPoint>>(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<DataPoint>) {
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"
}
}
}

View File

@ -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<DailyDetectionData>): 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<DailyDetectionData>): List<DataPoint> {
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<DataPoint> {
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<DailyDetectionData>
): List<PredictionDetail> {
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<PredictionDetail>()
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<String, Any>
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<DailyDetectionData>): 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<DailyDetectionData>,
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()
}
}

View File

@ -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<DataPoint> {
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<DailyDetectionData>
): List<PredictionDetail> {
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<PredictionDetail>()
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<String, Any>
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<DailyDetectionData>
): List<Pair<Int, String>> {
return predictNextSevenDaysDetail(lastDayNumber, detectionHistory)
.map { Pair(it.day, it.predictedSeverity) }
}
private fun calculateGadScoreTrend(history: List<DailyDetectionData>): 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<DailyDetectionData>,
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()
}
}

View File

@ -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"
}
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<FirebaseUser?> {
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<FirebaseUser?> {
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<Unit> {
return try {
FirebaseAuth.getInstance().sendPasswordResetEmail(email).await()
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
fun logoutUser() {
auth.signOut()
}
}

View File

@ -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<Unit> {
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<Unit> {
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<Unit> {
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<UserModel?> {
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<Unit> {
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<List<DiaryModel>> {
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<Unit> {
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<List<ShortDetectionModel>> {
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<String> {
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<List<Pair<String, RoutineDetectionModel>>> {
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<Pair<String, RoutineDetectionModel>?> {
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<String> {
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<Unit> {
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<String, DailyDetectionData>()
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<Unit> {
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<String, DailyDetectionData>()
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<Unit> {
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<List<VideoModel>> {
return try {
// Akses dokumen yoga langsung karena tidak ada kategori lain
val videoDocument = firestore.collection("video_store")
.document("yoga")
.get().await()
val videoList = mutableListOf<VideoModel>()
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 <T> addDocument(collection: String, documentId: String? = null, data: T): Result<Unit> {
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 <T> getDocumentById(collection: String, documentId: String, clazz: Class<T>): Result<T?> {
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<List<VideoModel>> {
// return try {
// val videoDocument = firestore.collection("video_store")
// .document(category)
// .get().await()
//
// val videoList = mutableListOf<VideoModel>()
// 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)
// }
// }
}

View File

@ -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
}
}
}
}

View File

@ -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<Preferences> 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<Boolean> = context.formDataStore.data.map { preferences ->
preferences[IS_SESSION_ACTIVE] ?: false
}
// Emosi yang dipilih
val emotion: Flow<String> = context.formDataStore.data.map { preferences ->
preferences[EMOTION] ?: ""
}
// Aktivitas yang dipilih
val activity: Flow<String> = context.formDataStore.data.map { preferences ->
preferences[ACTIVITY] ?: ""
}
// Langkah saat ini
val currentStep: Flow<String> = context.formDataStore.data.map { preferences ->
preferences[CURRENT_STEP] ?: "permission" // Default ke step pertama
}
// Total skor GAD-7
val gadTotalScore: Flow<Int> = context.formDataStore.data.map { preferences ->
preferences[GAD_TOTAL_SCORE] ?: 0
}
// Flow untuk tipe deteksi
val detectionType: Flow<String> = 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"
}
}

View File

@ -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<Preferences> 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<Boolean> = context.routineSessionDataStore.data
.map { preferences -> preferences[SESSION_ACTIVE] ?: false }
// Flow untuk memantau tipe sesi
val sessionType: Flow<String> = context.routineSessionDataStore.data
.map { preferences -> preferences[SESSION_TYPE] ?: "" }
// Flow untuk tanggal mulai sesi
val sessionStartDate: Flow<Long> = context.routineSessionDataStore.data
.map { preferences -> preferences[SESSION_START_DATE] ?: 0L }
// Flow untuk tanggal akhir sesi
val sessionEndDate: Flow<Long> = context.routineSessionDataStore.data
.map { preferences -> preferences[SESSION_END_DATE] ?: 0L }
// Flow untuk tanggal terakhir pengisian form
val lastFormCompletionDate: Flow<String> = 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<Int, Int> {
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
}
}
}

View File

@ -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<String?> = context.dataStore.data.map { preferences ->
preferences[TOKEN_KEY]
}
// Ambil user ID
val sessionUserId: Flow<String?> = context.dataStore.data.map { preferences ->
preferences[USER_ID_KEY]
}
// Ambil waktu kadaluarsa session
val sessionExpiresAt: Flow<Long?> = 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")
}
}
}

View File

@ -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<UserEntity?>
@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()
}

View File

@ -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
)
}

View File

@ -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<String, Any> = emptyMap(),
var tanggal: Timestamp = Timestamp.now()
) {
constructor() : this(emptyMap(), Timestamp.now())
}

View File

@ -0,0 +1,5 @@
package com.example.app_skripsi.data.model
data class FaqModel(val question: String,
val answer: String,
var isExpanded: Boolean = false)

View File

@ -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<String, DailyDetectionData> = 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 = ""
)

View File

@ -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 = ""
)

View File

@ -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)
}

View File

@ -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

View File

@ -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<Int>,
totalScore: Int
): Result<Unit> {
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<List<ShortDetectionModel>> {
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<Int>,
totalScore: Int
): Result<String> {
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<Int>,
totalScore: Int,
routineSessionManager: RoutineSessionManager
): Result<Unit> {
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<Int> {
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<String> {
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<Int>,
totalScore: Int,
routineSessionManager: RoutineSessionManager // Add this parameter
): Result<Unit> {
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<Int>,
totalScore: Int
): Result<String> {
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<Pair<String, RoutineDetectionModel>?> {
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<Unit> {
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<Boolean> {
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<Int> {
// 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<Int> {
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<Boolean> {
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"
}
}
}

View File

@ -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<Unit> {
return firebaseService.registerUser(email, password, user)
}
suspend fun loginUser(email: String, password: String): Result<Unit> {
return firebaseService.loginUser(email, password)
}
suspend fun sendPasswordResetEmail(email: String): Result<Unit> {
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<UserModel?> {
return firebaseService.getDocumentById(
collection = "users_students/data/userId",
documentId = userId,
clazz = UserModel::class.java
)
}
}

View File

@ -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<Unit> {
val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in"))
return firebaseService.addDiary(userId, diary)
}
suspend fun getDiaries(): Result<List<DiaryModel>> {
val userId = firebaseService.getCurrentUserId() ?: return Result.failure(Exception("User not logged in"))
return firebaseService.getDiaries(userId)
}
}

View File

@ -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<UserModel?> {
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<Unit> {
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")
}
}

View File

@ -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<List<VideoModel>> {
return firebaseService.getVideos()
}
// Tetap sediakan metode ini untuk kompatibilitas jika diperlukan
suspend fun getVideosByCategory(category: String): Result<List<VideoModel>> {
// Abaikan kategori, ambil semua video
return firebaseService.getVideos()
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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<PredictionNotificationWorker>()
.build()
WorkManager.getInstance(context).enqueue(workRequest)
// Schedule next alarm for tomorrow
val schedulerManager = NotificationSchedulerManager(context)
schedulerManager.scheduleDailyPredictionNotificationWithAlarm(8, 0)
}
}

View File

@ -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<RoutineFormReminderWorker>()
.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)
}
}
}

View File

@ -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<FirebaseUser?>()
val user: LiveData<FirebaseUser?> get() = _user
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> get() = _loading
fun checkCurrentUser() {
_user.value = authService.getCurrentUser()
}
fun logout() {
authService.logoutUser()
_user.value = null
}
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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")
}
}
}

View File

@ -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<Result<Unit>>()
val resetResult: LiveData<Result<Unit>> get() = _resetResult
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> get() = _loading
fun sendPasswordReset(email: String) {
_loading.value = true
viewModelScope.launch {
val result = authRepository.sendPasswordResetEmail(email)
_resetResult.value = result
_loading.value = false
}
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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<Result<UserEntity?>>()
val loginResult: LiveData<Result<UserEntity?>> get() = _loginResult
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> 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")

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(LoginViewModel::class.java) -> {
LoginViewModel(application, authRepository, userRepository) as T
}
else -> throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

View File

@ -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
}
}

View File

@ -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<Result<Unit>>()
val registerResult: LiveData<Result<Unit>> get() = _registerResult
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> 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
}
}
}

View File

@ -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<Pair<String, DailyDetectionData>>
) : RecyclerView.Adapter<DailyDetectionAdapter.ViewHolder>() {
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
}

View File

@ -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<String, Int, Int>) {
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<RadioGroup>(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"
}
}

View File

@ -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]
}
}

View File

@ -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<String>()
val selectedEmotion: LiveData<String> get() = _selectedEmotion
private val _selectedActivity = MutableLiveData<String>()
val selectedActivity: LiveData<String> get() = _selectedActivity
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> get() = _errorMessage
private val _navigateToResult = MutableLiveData<Boolean>()
val navigateToResult: LiveData<Boolean> get() = _navigateToResult
private val _activeRoutineDetection = MutableLiveData<Pair<String, RoutineDetectionModel>?>()
val activeRoutineDetection: LiveData<Pair<String, RoutineDetectionModel>?> get() = _activeRoutineDetection
private val _routineSessionInfo = MutableLiveData<Triple<String, Int, Int>>() // type, currentDay, totalDays
val routineSessionInfo: LiveData<Triple<String, Int, Int>> get() = _routineSessionInfo
// LiveData untuk menyimpan jawaban GAD untuk diteruskan ke activity hasil
private val _gadAnswersList = MutableLiveData<ArrayList<Int>>()
val gadAnswersList: LiveData<ArrayList<Int>> 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<Int>, 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<String, Triple<Int, Int, String>>? {
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<Int?>): 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<Int>()
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<Int>
)
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(FormAnxietyViewModel::class.java)) {
return FormAnxietyViewModel(
anxietyRepository,
formSessionManager,
routineSessionManager
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@ -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<Int>()
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<Int>
) {
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<Int>
) {
// 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<Int>) {
// 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
}
}

View File

@ -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<String, DailyDetectionData> to List<Pair<String, DailyDetectionData>>
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("<b>Riwayat Tingkat Kecemasan:</b> ")
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("<font color='$colorCode'>$hari: $severity</font>")
}
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("<b>Prediksi 7 Hari ke Depan (Berdasarkan Data Personal Anda):</b><br>")
predictionDetails.forEachIndexed { index, detail ->
// Add separator for days
if (index > 0) predictionHtml.append("<br>")
// 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("<b>${index+1}. ${detail.dayOfWeek} $confidenceIndicator:</b> ")
predictionHtml.append("Prediksi: <font color='$severityColor'>${detail.predictedSeverity}</font> (Skor: ${detail.predictedGadScore})<br>")
// Add personalized description
predictionHtml.append(detail.getReadableDescription())
// Add recommendation
predictionHtml.append("<br><i>Rekomendasi:</i> ${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
}
}

View File

@ -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<ViewPager2>(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<MaterialCardView, Pair<ImageView, TextView>>) {
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
}
}

View File

@ -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<ImageView, String>
// 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<ViewPager2>(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 {
}
}
}
}

View File

@ -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<Int?>(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<Int?>(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<Int>()
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<ViewPager2>(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)
}
}
}
}

View File

@ -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<Int>? = 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
}
}

View File

@ -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<ViewPager2>(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 {
}
}
}
}

View File

@ -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<ShortDetectionModel>) :
RecyclerView.Adapter<ShortDetectionAdapter.ShortDetectionViewHolder>() {
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)
}
}
}
}

View File

@ -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<List<ShortDetectionModel>>()
val shortDetectionList: LiveData<List<ShortDetectionModel>> 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
}
}
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ShortDetectionViewModel::class.java)) {
return ShortDetectionViewModel(
anxietyRepository,
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@ -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()
}
}

View File

@ -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]
}

View File

@ -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<String>()
val userName: LiveData<String> get() = _userName
private val _userEmail = MutableLiveData<String>()
val userEmail: LiveData<String> 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"
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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<VideoModel>,
private val onItemClick: (VideoModel) -> Unit
) : RecyclerView.Adapter<VideoAdapter.VideoViewHolder>() {
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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<List<VideoModel>>()
val videos: LiveData<List<VideoModel>> = _videos
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _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
}
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(VideoViewModel::class.java)) {
return VideoViewModel(videoRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@ -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<DailyNotificationWorker>()
.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<PeriodicNotificationWorker>(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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<String>()
val selectedEmotion: LiveData<String> get() = _selectedEmotion
private val _diaries = MutableLiveData<List<DiaryModel>>()
val diaries: LiveData<List<DiaryModel>> get() = _diaries
private val _diarySaveResult = MutableLiveData<Result<Unit>>()
val diarySaveResult: LiveData<Result<Unit>> get() = _diarySaveResult
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> 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
}
}
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(DiaryViewModel::class.java) -> {
DiaryViewModel(diaryRepository) as T
}
else -> throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

View File

@ -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<DiaryModel>) :
RecyclerView.Adapter<DiaryAdapter.DiaryViewHolder>() {
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<DiaryModel>) {
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)
}
}

View File

@ -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<DiaryModel>,
private val newList: List<DiaryModel>
) : 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()
}
}

View File

@ -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<ImageView, String>
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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<FaqModel>) : RecyclerView.Adapter<FaqAdapter.FaqViewHolder>() {
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
}

Some files were not shown because too many files have changed in this diff Show More