initialize commit
This commit is contained in:
commit
e936b525df
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
|
@ -0,0 +1 @@
|
|||
App_Skripsi
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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>();
|
||||
#}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.example.app_skripsi.data.model
|
||||
|
||||
data class FaqModel(val question: String,
|
||||
val answer: String,
|
||||
var isExpanded: Boolean = false)
|
|
@ -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 = ""
|
||||
)
|
|
@ -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 = ""
|
||||
)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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")
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue