diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..71d8cef 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef7126a..dc0ece6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ + + = _totalSteps.asStateFlow() + + private val _stepDetected = MutableStateFlow(false) + val stepDetected: StateFlow = _stepDetected.asStateFlow() + + private val _isWalking = MutableStateFlow(false) + val isWalking: StateFlow = _isWalking.asStateFlow() + + private val _sensorStatus = MutableStateFlow("") + val sensorStatus: StateFlow = _sensorStatus.asStateFlow() + + // Internal state + private var baselineSteps = -1 // Changed to -1 for better initial detection + private var sessionSteps = 0 + private var detectorStepCount = 0 + private var lastStepTime = 0L + private var isTracking = false + + // Sensor availability flags + private var hasStepCounter = false + private var hasStepDetector = false + + companion object { + private const val TAG = "ImprovedStepManager" + private const val WALKING_TIMEOUT = 2000L + } + + init { + checkSensorAvailability() + } + + private fun checkSensorAvailability() { + hasStepCounter = stepCounterSensor != null + hasStepDetector = stepDetectorSensor != null + + Log.d(TAG, "=== SENSOR CHECK ===") + Log.d(TAG, "STEP_COUNTER available: $hasStepCounter") + Log.d(TAG, "STEP_DETECTOR available: $hasStepDetector") + + if (hasStepCounter) { + Log.d(TAG, "STEP_COUNTER: ${stepCounterSensor?.name} (${stepCounterSensor?.vendor})") + } + if (hasStepDetector) { + Log.d(TAG, "STEP_DETECTOR: ${stepDetectorSensor?.name} (${stepDetectorSensor?.vendor})") + } + + updateSensorStatus() + } + + private fun updateSensorStatus() { + _sensorStatus.value = when { + hasStepCounter && hasStepDetector -> "Hybrid Mode (Counter + Detector)" + hasStepDetector -> "Detector Only Mode" + hasStepCounter -> "Counter Only Mode" + else -> "No Sensor Available" + } + } + + fun isSensorAvailable(): Boolean { + return hasStepCounter || hasStepDetector + } + + fun startTracking() { + if (isTracking) { + Log.w(TAG, "Already tracking!") + return + } + + Log.d(TAG, "=== START TRACKING ===") + isTracking = true + + var sensorsRegistered = 0 + + // Register STEP_DETECTOR first (priority for responsiveness) + if (hasStepDetector) { + val success = sensorManager.registerListener( + this, + stepDetectorSensor, + SensorManager.SENSOR_DELAY_FASTEST + ) + Log.d(TAG, "STEP_DETECTOR registration: ${if (success) "SUCCESS" else "FAILED"}") + if (success) sensorsRegistered++ + } + + // Register STEP_COUNTER for accuracy + if (hasStepCounter) { + val success = sensorManager.registerListener( + this, + stepCounterSensor, + SensorManager.SENSOR_DELAY_UI + ) + Log.d(TAG, "STEP_COUNTER registration: ${if (success) "SUCCESS" else "FAILED"}") + if (success) sensorsRegistered++ + } + + Log.d(TAG, "Total sensors registered: $sensorsRegistered") + + if (sensorsRegistered == 0) { + Log.e(TAG, "NO SENSORS REGISTERED!") + isTracking = false + } + } + + fun stopTracking() { + if (!isTracking) return + + Log.d(TAG, "=== STOP TRACKING ===") + sensorManager.unregisterListener(this) + isTracking = false + _isWalking.value = false + } + + fun resetSteps() { + Log.d(TAG, "=== RESET STEPS ===") + baselineSteps = -1 + sessionSteps = 0 + detectorStepCount = 0 + _totalSteps.value = 0 + lastStepTime = 0L + } + + override fun onSensorChanged(event: SensorEvent?) { + if (!isTracking) return + + event?.let { + when (it.sensor.type) { + Sensor.TYPE_STEP_COUNTER -> handleStepCounter(it) + Sensor.TYPE_STEP_DETECTOR -> handleStepDetector(it) + } + } + } + + private fun handleStepCounter(event: SensorEvent) { + val rawSteps = event.values[0].toInt() + + if (baselineSteps == -1) { + // First reading - set baseline + baselineSteps = rawSteps + Log.d(TAG, "STEP_COUNTER: Baseline set to $baselineSteps") + + // If we have detector, use its count + if (hasStepDetector && detectorStepCount > 0) { + sessionSteps = detectorStepCount + Log.d(TAG, "STEP_COUNTER: Using detector count $detectorStepCount") + } else { + sessionSteps = 0 + } + } else { + // Calculate session steps + val counterSessionSteps = rawSteps - baselineSteps + + // If we have detector, use detector count (more accurate for short sessions) + // Otherwise use counter + if (hasStepDetector && detectorStepCount > 0) { + sessionSteps = detectorStepCount + Log.d(TAG, "STEP_COUNTER: Raw=$rawSteps, Counter=${counterSessionSteps}, Using Detector=$detectorStepCount") + } else { + sessionSteps = counterSessionSteps + Log.d(TAG, "STEP_COUNTER: Raw=$rawSteps, Session=$sessionSteps") + } + } + + _totalSteps.value = sessionSteps + } + + private fun handleStepDetector(event: SensorEvent) { + val currentTime = System.currentTimeMillis() + + // Increment counter + detectorStepCount++ + sessionSteps = detectorStepCount + + // Update total immediately + _totalSteps.value = sessionSteps + + // Update walking state + _isWalking.value = true + lastStepTime = currentTime + + // Trigger visual feedback + _stepDetected.value = true + + Log.d(TAG, "STEP_DETECTOR: Step #$detectorStepCount detected!") + + // Reset visual feedback after animation + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + _stepDetected.value = false + }, 200) + + // Schedule walking timeout check + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + checkWalkingTimeout(currentTime) + }, WALKING_TIMEOUT) + } + + private fun checkWalkingTimeout(stepTime: Long) { + if (stepTime == lastStepTime) { + val timeSinceLastStep = System.currentTimeMillis() - lastStepTime + if (timeSinceLastStep >= WALKING_TIMEOUT) { + _isWalking.value = false + Log.d(TAG, "Walking stopped (timeout)") + } + } + } + + fun getDebugInfo(): String { + return """ + Sensor Status: ${_sensorStatus.value} + Tracking: $isTracking + Total Steps: ${_totalSteps.value} + Detector Count: $detectorStepCount + Counter Baseline: $baselineSteps + Walking: ${_isWalking.value} + """.trimIndent() + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + val accuracyStr = when (accuracy) { + SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> "HIGH" + SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> "MEDIUM" + SensorManager.SENSOR_STATUS_ACCURACY_LOW -> "LOW" + SensorManager.SENSOR_STATUS_NO_CONTACT -> "NO_CONTACT" + SensorManager.SENSOR_STATUS_UNRELIABLE -> "UNRELIABLE" + else -> "UNKNOWN" + } + Log.d(TAG, "Sensor accuracy changed: ${sensor?.name} = $accuracyStr") + } +} \ No newline at end of file diff --git a/app/src/main/java/ui/screen/steps/StepsScreen.kt b/app/src/main/java/ui/screen/steps/StepsScreen.kt index 86912f7..ec89d86 100644 --- a/app/src/main/java/ui/screen/steps/StepsScreen.kt +++ b/app/src/main/java/ui/screen/steps/StepsScreen.kt @@ -1,9 +1,14 @@ package com.example.stepdrink.ui.screen.steps import android.Manifest +import android.content.Context import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -18,8 +23,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -31,40 +38,107 @@ fun StepsScreen( navController: NavController, viewModel: StepViewModel ) { + val context = LocalContext.current + val todaySteps by viewModel.todaySteps.collectAsState() val stepGoal by viewModel.dailyGoal.collectAsState() - val sensorSteps by viewModel.stepCounterManager.totalSteps.collectAsState() + val sensorSteps by viewModel.improvedStepManager.totalSteps.collectAsState() val last7Days by viewModel.last7DaysSteps.collectAsState() - val userWeight by viewModel.userWeight.collectAsState() // TAMBAH INI + val userWeight by viewModel.userWeight.collectAsState() + val sensorStatus by viewModel.improvedStepManager.sensorStatus.collectAsState() + + // Real-time detection states + val stepDetected by viewModel.improvedStepManager.stepDetected.collectAsState() + val isWalking by viewModel.improvedStepManager.isWalking.collectAsState() var isTracking by remember { mutableStateOf(false) } var permissionGranted by remember { mutableStateOf(false) } + var showDebugInfo by remember { mutableStateOf(false) } - // Calculate calories - TAMBAH INI + // Calculate calories val todayCalories = remember(todaySteps?.steps, userWeight) { viewModel.calculateCalories(todaySteps?.steps ?: 0, userWeight) } + // Log when sensor steps change + LaunchedEffect(sensorSteps) { + Log.d("StepsScreen", "Sensor steps: $sensorSteps") + } + + // Haptic feedback + LaunchedEffect(stepDetected) { + if (stepDetected && isTracking) { + try { + val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator?.vibrate( + VibrationEffect.createOneShot(30, VibrationEffect.DEFAULT_AMPLITUDE) + ) + } else { + @Suppress("DEPRECATION") + vibrator?.vibrate(30) + } + } catch (e: Exception) { + Log.e("StepsScreen", "Vibration error: ${e.message}") + } + } + } + // Permission launcher val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> permissionGranted = isGranted + Log.d("StepsScreen", "Permission result: $isGranted") + if (isGranted) { - viewModel.stepCounterManager.startTracking() + viewModel.improvedStepManager.startTracking() isTracking = true + Log.d("StepsScreen", "Tracking started") + } else { + Log.w("StepsScreen", "Permission denied!") } } Scaffold( topBar = { TopAppBar( - title = { Text("Pelacak Langkah") }, + title = { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Pelacak Langkah") + + if (isWalking && isTracking) { + PulsingDot() + } + } + + // Sensor status indicator + Text( + text = sensorStatus, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + ) + } + }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, "Kembali") } }, + actions = { + // Debug info button + IconButton(onClick = { showDebugInfo = !showDebugInfo }) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "Debug Info", + tint = if (showDebugInfo) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onPrimaryContainer + ) + } + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -75,13 +149,15 @@ fun StepsScreen( ExtendedFloatingActionButton( onClick = { if (isTracking) { - viewModel.stepCounterManager.stopTracking() + Log.d("StepsScreen", "Stopping tracking") + viewModel.improvedStepManager.stopTracking() isTracking = false } else { + Log.d("StepsScreen", "Requesting permission") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { permissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION) } else { - viewModel.stepCounterManager.startTracking() + viewModel.improvedStepManager.startTracking() isTracking = true } } @@ -97,183 +173,328 @@ fun StepsScreen( ) } ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - item { - // Main Steps Card dengan Gradient - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = Color.Transparent - ), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color(0xFF4CAF50), - Color(0xFF81C784) - ) - ) - ) - .padding(24.dp) - ) { - Column( + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Debug Info Card (collapsible) + if (showDebugInfo) { + item { + Card( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) ) { - // Animated Icon - if (isTracking) { - AnimatedWalkingIcon() - } else { - Icon( - imageVector = Icons.Default.DirectionsWalk, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = Color.White + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "🐛 Debug Info", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = viewModel.getDebugInfo(), + style = MaterialTheme.typography.bodySmall, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace ) } + } + } + } - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "${todaySteps?.steps ?: 0}", - style = MaterialTheme.typography.displayLarge, - fontWeight = FontWeight.Bold, - color = Color.White - ) - - Text( - text = "Langkah Hari Ini", - style = MaterialTheme.typography.titleMedium, - color = Color.White.copy(alpha = 0.9f) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - LinearProgressIndicator( - progress = { - (todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat() - }, - modifier = Modifier - .fillMaxWidth() - .height(10.dp) - .clip(MaterialTheme.shapes.small), - color = Color.White, - trackColor = Color.White.copy(alpha = 0.3f), - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Target: $stepGoal langkah", - style = MaterialTheme.typography.bodyMedium, - color = Color.White.copy(alpha = 0.9f) - ) - - if (isTracking) { - Spacer(modifier = Modifier.height(12.dp)) - Surface( - shape = MaterialTheme.shapes.small, - color = Color.White.copy(alpha = 0.2f) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - PulsingDot() - Text( - "Sedang Tracking: $sensorSteps langkah", - color = Color.White, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold + item { + // Main Steps Card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.Transparent + ), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFF4CAF50), + Color(0xFF81C784) ) + ) + ) + .padding(24.dp) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (isTracking && isWalking) { + AnimatedWalkingIcon() + } else { + Icon( + imageVector = Icons.Default.DirectionsWalk, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Display sensor steps in real-time when tracking + Text( + text = if (isTracking) "$sensorSteps" else "${todaySteps?.steps ?: 0}", + style = MaterialTheme.typography.displayLarge, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + Text( + text = "Langkah Hari Ini", + style = MaterialTheme.typography.titleMedium, + color = Color.White.copy(alpha = 0.9f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + LinearProgressIndicator( + progress = { + val steps = if (isTracking) sensorSteps else (todaySteps?.steps ?: 0) + steps.toFloat() / stepGoal.toFloat() + }, + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .clip(MaterialTheme.shapes.small), + color = Color.White, + trackColor = Color.White.copy(alpha = 0.3f), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Target: $stepGoal langkah", + style = MaterialTheme.typography.bodyMedium, + color = Color.White.copy(alpha = 0.9f) + ) + + if (isTracking) { + Spacer(modifier = Modifier.height(12.dp)) + Surface( + shape = MaterialTheme.shapes.small, + color = Color.White.copy(alpha = 0.2f) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Sensors, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(16.dp) + ) + Text( + "Tracking Aktif • Real-time", + color = Color.White, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold + ) + } } } } } } } - } - // TAMBAH INI - Calories Card - item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = Color.Transparent - ), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background( - brush = Brush.horizontalGradient( - colors = listOf( - Color(0xFFFF6B6B), - Color(0xFFFF8E53) + // Calories Card + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color.Transparent + ), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.horizontalGradient( + colors = listOf( + Color(0xFFFF6B6B), + Color(0xFFFF8E53) + ) ) ) - ) - .padding(20.dp) + .padding(20.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(56.dp) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.2f)), + contentAlignment = Alignment.Center + ) { + Text( + text = "🔥", + style = MaterialTheme.typography.headlineMedium + ) + } + Column { + Text( + text = "Kalori Terbakar", + style = MaterialTheme.typography.titleMedium, + color = Color.White.copy(alpha = 0.9f) + ) + Text( + text = "$todayCalories kkal", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + Column( + horizontalAlignment = Alignment.End + ) { + Text( + text = "Berat: ${userWeight}kg", + style = MaterialTheme.typography.bodySmall, + color = Color.White.copy(alpha = 0.8f) + ) + Text( + text = "≈ ${String.format("%.1f", todayCalories / 7.7)}g lemak", + style = MaterialTheme.typography.bodySmall, + color = Color.White.copy(alpha = 0.8f) + ) + } + } + } + } + } + + // Rest of the items (Motivational message, history, etc.) + item { + val message = when { + (todaySteps?.steps ?: 0) >= stepGoal -> "🎉 Luar biasa! Target tercapai!" + (todaySteps?.steps ?: 0) >= (stepGoal * 0.75) -> "💪 Hampir sampai! Tetap semangat!" + (todaySteps?.steps ?: 0) >= (stepGoal * 0.5) -> "🚶 Setengah jalan! Ayo lanjutkan!" + (todaySteps?.steps ?: 0) >= (stepGoal * 0.25) -> "👟 Awal yang bagus! Terus bergerak!" + else -> "🌟 Yuk mulai bergerak hari ini!" + } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) + ) { + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp), + fontWeight = FontWeight.Medium + ) + } + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Riwayat 7 Hari Terakhir", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Icon( + imageVector = Icons.Default.History, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } + + items(last7Days) { record -> + val recordCalories = viewModel.calculateCalories(record.steps, userWeight) + + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { - Box( - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background(Color.White.copy(alpha = 0.2f)), - contentAlignment = Alignment.Center + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.size(48.dp) ) { - Text( - text = "🔥", - style = MaterialTheme.typography.headlineMedium - ) + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Default.CalendarToday, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + } } Column { Text( - text = "Kalori Terbakar", + text = record.date, style = MaterialTheme.typography.titleMedium, - color = Color.White.copy(alpha = 0.9f) + fontWeight = FontWeight.Bold ) Text( - text = "$todayCalories kkal", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = Color.White + text = "${record.steps} langkah • $recordCalories kkal", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } + Column( horizontalAlignment = Alignment.End ) { - Text( - text = "Berat: ${userWeight}kg", - style = MaterialTheme.typography.bodySmall, - color = Color.White.copy(alpha = 0.8f) + CircularProgressIndicator( + progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) }, + modifier = Modifier.size(48.dp), + strokeWidth = 4.dp ) + Spacer(modifier = Modifier.height(4.dp)) Text( - text = "≈ ${String.format("%.2f", todayCalories / 7.7)} gram lemak", - style = MaterialTheme.typography.bodySmall, - color = Color.White.copy(alpha = 0.8f) + text = "${((record.steps.toFloat() / stepGoal.toFloat()) * 100).toInt()}%", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold ) } } @@ -281,111 +502,24 @@ fun StepsScreen( } } - // Motivational Message - TAMBAH INI - item { - val message = when { - (todaySteps?.steps ?: 0) >= stepGoal -> "🎉 Luar biasa! Target tercapai!" - (todaySteps?.steps ?: 0) >= (stepGoal * 0.75) -> "💪 Hampir sampai! Tetap semangat!" - (todaySteps?.steps ?: 0) >= (stepGoal * 0.5) -> "🚶 Setengah jalan! Ayo lanjutkan!" - (todaySteps?.steps ?: 0) >= (stepGoal * 0.25) -> "👟 Awal yang bagus! Terus bergerak!" - else -> "🌟 Yuk mulai bergerak hari ini!" - } - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer - ) + // Floating step indicator + AnimatedVisibility( + visible = stepDetected && isTracking, + enter = scaleIn(initialScale = 0.5f) + fadeIn(), + exit = scaleOut(targetScale = 1.5f) + fadeOut(), + modifier = Modifier.align(Alignment.Center) + ) { + Surface( + shape = CircleShape, + color = Color.White, + shadowElevation = 8.dp, + modifier = Modifier.size(80.dp) ) { - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(16.dp), - fontWeight = FontWeight.Medium - ) - } - } - - item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Riwayat 7 Hari Terakhir", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - Icon( - imageVector = Icons.Default.History, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - } - - items(last7Days) { record -> - val recordCalories = viewModel.calculateCalories(record.steps, userWeight) - - Card( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Surface( - shape = CircleShape, - color = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier.size(48.dp) - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - imageVector = Icons.Default.CalendarToday, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } - } - Column { - Text( - text = record.date, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Text( - text = "${record.steps} langkah • $recordCalories kkal", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Column( - horizontalAlignment = Alignment.End - ) { - CircularProgressIndicator( - progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) }, - modifier = Modifier.size(48.dp), - strokeWidth = 4.dp - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "${((record.steps.toFloat() / stepGoal.toFloat()) * 100).toInt()}%", - style = MaterialTheme.typography.labelSmall, - fontWeight = FontWeight.Bold - ) - } + Box(contentAlignment = Alignment.Center) { + Text( + text = "👟", + style = MaterialTheme.typography.displaySmall + ) } } } @@ -400,7 +534,7 @@ fun AnimatedWalkingIcon() { initialValue = -10f, targetValue = 10f, animationSpec = infiniteRepeatable( - animation = tween(500, easing = LinearEasing), + animation = tween(400, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "rotation" @@ -419,20 +553,21 @@ fun AnimatedWalkingIcon() { @Composable fun PulsingDot() { val infiniteTransition = rememberInfiniteTransition(label = "pulse") - val alpha by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0.3f, + val scale by infiniteTransition.animateFloat( + initialValue = 0.8f, + targetValue = 1.2f, animationSpec = infiniteRepeatable( - animation = tween(1000, easing = LinearEasing), + animation = tween(600, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), - label = "alpha" + label = "scale" ) Box( modifier = Modifier .size(8.dp) + .scale(scale) .clip(CircleShape) - .background(Color.White.copy(alpha = alpha)) + .background(Color(0xFF4CAF50)) ) } \ No newline at end of file diff --git a/app/src/main/java/viewmodel/StepViewModel.kt b/app/src/main/java/viewmodel/StepViewModel.kt index c6cdd4c..565c0d1 100644 --- a/app/src/main/java/viewmodel/StepViewModel.kt +++ b/app/src/main/java/viewmodel/StepViewModel.kt @@ -1,22 +1,22 @@ package com.example.stepdrink.viewmodel import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.example.stepdrink.data.local.PreferencesManager import com.example.stepdrink.data.local.database.AppDatabase import com.example.stepdrink.data.local.entity.StepRecord import com.example.stepdrink.data.repository.StepRepository -import com.example.stepdrink.sensor.StepCounterManager +import com.example.stepdrink.sensor.ImprovedStepManager import com.example.stepdrink.util.DateUtils import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class StepViewModel(application: Application) : AndroidViewModel(application) { - // PERBAIKAN: Initialize repository dulu di init block private val repository: StepRepository - val stepCounterManager: StepCounterManager + val improvedStepManager: ImprovedStepManager private val preferencesManager = PreferencesManager(application) val dailyGoal: StateFlow @@ -24,11 +24,19 @@ class StepViewModel(application: Application) : AndroidViewModel(application) { val todaySteps: StateFlow val last7DaysSteps: StateFlow> + companion object { + private const val TAG = "StepViewModel" + } + init { + Log.d(TAG, "=== VIEWMODEL INIT ===") + // Initialize database & repository val database = AppDatabase.getDatabase(application) repository = StepRepository(database.stepDao()) - stepCounterManager = StepCounterManager(application) + improvedStepManager = ImprovedStepManager(application) + + Log.d(TAG, "Sensor available: ${improvedStepManager.isSensorAvailable()}") // Initialize flows dailyGoal = preferencesManager.stepGoal @@ -45,16 +53,25 @@ class StepViewModel(application: Application) : AndroidViewModel(application) { // Observe sensor dan update database viewModelScope.launch { - stepCounterManager.totalSteps.collect { steps -> + improvedStepManager.totalSteps.collect { steps -> + Log.d(TAG, "Steps updated: $steps") if (steps > 0) { updateTodaySteps(steps) } } } + + // Log sensor status + viewModelScope.launch { + improvedStepManager.sensorStatus.collect { status -> + Log.d(TAG, "Sensor status: $status") + } + } } private fun updateTodaySteps(steps: Int) { viewModelScope.launch { + Log.d(TAG, "Updating database: $steps steps for ${DateUtils.getCurrentDate()}") repository.insertOrUpdateSteps(DateUtils.getCurrentDate(), steps) } } @@ -64,22 +81,23 @@ class StepViewModel(application: Application) : AndroidViewModel(application) { return (today.toFloat() / dailyGoal.value.toFloat()).coerceIn(0f, 1f) } - // Function untuk hitung kalori fun calculateCalories(steps: Int, weight: Int): Int { - // Rumus: (Steps × Weight × 0.57) / 1000 - // Contoh: 10000 steps × 70kg = 399 kalori return ((steps * weight * 0.57) / 1000).toInt() } - // Get kalori untuk hari ini fun getTodayCalories(): Int { val steps = todaySteps.value?.steps ?: 0 val weight = userWeight.value return calculateCalories(steps, weight) } + fun getDebugInfo(): String { + return improvedStepManager.getDebugInfo() + } + override fun onCleared() { super.onCleared() - stepCounterManager.stopTracking() + Log.d(TAG, "=== VIEWMODEL CLEARED ===") + improvedStepManager.stopTracking() } } \ No newline at end of file