sensor update

This commit is contained in:
HagaDalpintoGinting 2025-12-18 12:38:48 +07:00
parent a23005280b
commit acc4e48a1a
6 changed files with 702 additions and 267 deletions

View File

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-12-18T05:31:18.470754700Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=RRCX303Z3VY" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@ -7,6 +7,8 @@
<!-- Permission untuk notifikasi (opsional, untuk reminder minum) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Require step counter sensor -->
<uses-feature
android:name="android.hardware.sensor.stepcounter"

View File

@ -0,0 +1,259 @@
package com.example.stepdrink.sensor
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* ImprovedStepManager - Fixed version untuk Samsung dan device lain
*
* Improvements:
* - Better sensor detection
* - Automatic fallback
* - More logging
* - Samsung compatibility fixes
*/
class ImprovedStepManager(context: Context) : SensorEventListener {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
// Try both sensors
private val stepCounterSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
private val stepDetectorSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR)
// Flow states
private val _totalSteps = MutableStateFlow(0)
val totalSteps: StateFlow<Int> = _totalSteps.asStateFlow()
private val _stepDetected = MutableStateFlow(false)
val stepDetected: StateFlow<Boolean> = _stepDetected.asStateFlow()
private val _isWalking = MutableStateFlow(false)
val isWalking: StateFlow<Boolean> = _isWalking.asStateFlow()
private val _sensorStatus = MutableStateFlow("")
val sensorStatus: StateFlow<String> = _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")
}
}

View File

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

View File

@ -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<Int>
@ -24,11 +24,19 @@ class StepViewModel(application: Application) : AndroidViewModel(application) {
val todaySteps: StateFlow<StepRecord?>
val last7DaysSteps: StateFlow<List<StepRecord>>
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()
}
}