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> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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> </SelectionState>
</selectionStates> </selectionStates>
</component> </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) --> <!-- Permission untuk notifikasi (opsional, untuk reminder minum) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Require step counter sensor --> <!-- Require step counter sensor -->
<uses-feature <uses-feature
android:name="android.hardware.sensor.stepcounter" 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 package com.example.stepdrink.ui.screen.steps
import android.Manifest import android.Manifest
import android.content.Context
import android.os.Build import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -18,8 +23,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
@ -31,40 +38,107 @@ fun StepsScreen(
navController: NavController, navController: NavController,
viewModel: StepViewModel viewModel: StepViewModel
) { ) {
val context = LocalContext.current
val todaySteps by viewModel.todaySteps.collectAsState() val todaySteps by viewModel.todaySteps.collectAsState()
val stepGoal by viewModel.dailyGoal.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 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 isTracking by remember { mutableStateOf(false) }
var permissionGranted 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) { val todayCalories = remember(todaySteps?.steps, userWeight) {
viewModel.calculateCalories(todaySteps?.steps ?: 0, 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 // Permission launcher
val permissionLauncher = rememberLauncherForActivityResult( val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted -> ) { isGranted ->
permissionGranted = isGranted permissionGranted = isGranted
Log.d("StepsScreen", "Permission result: $isGranted")
if (isGranted) { if (isGranted) {
viewModel.stepCounterManager.startTracking() viewModel.improvedStepManager.startTracking()
isTracking = true isTracking = true
Log.d("StepsScreen", "Tracking started")
} else {
Log.w("StepsScreen", "Permission denied!")
} }
} }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( 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 = { navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, "Kembali") 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( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
@ -75,13 +149,15 @@ fun StepsScreen(
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { onClick = {
if (isTracking) { if (isTracking) {
viewModel.stepCounterManager.stopTracking() Log.d("StepsScreen", "Stopping tracking")
viewModel.improvedStepManager.stopTracking()
isTracking = false isTracking = false
} else { } else {
Log.d("StepsScreen", "Requesting permission")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
permissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION) permissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION)
} else { } else {
viewModel.stepCounterManager.startTracking() viewModel.improvedStepManager.startTracking()
isTracking = true isTracking = true
} }
} }
@ -97,183 +173,328 @@ fun StepsScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( Box(modifier = Modifier.fillMaxSize()) {
modifier = Modifier LazyColumn(
.fillMaxSize() modifier = Modifier
.padding(paddingValues) .fillMaxSize()
.padding(16.dp), .padding(paddingValues)
verticalArrangement = Arrangement.spacedBy(16.dp) .padding(16.dp),
) { verticalArrangement = Arrangement.spacedBy(16.dp)
item { ) {
// Main Steps Card dengan Gradient // Debug Info Card (collapsible)
Card( if (showDebugInfo) {
modifier = Modifier.fillMaxWidth(), item {
colors = CardDefaults.cardColors( Card(
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(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) { ) {
// Animated Icon Column(
if (isTracking) { modifier = Modifier.padding(16.dp)
AnimatedWalkingIcon() ) {
} else { Text(
Icon( text = "🐛 Debug Info",
imageVector = Icons.Default.DirectionsWalk, style = MaterialTheme.typography.titleMedium,
contentDescription = null, fontWeight = FontWeight.Bold
modifier = Modifier.size(64.dp), )
tint = Color.White 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)) item {
// Main Steps Card
Text( Card(
text = "${todaySteps?.steps ?: 0}", modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.displayLarge, colors = CardDefaults.cardColors(
fontWeight = FontWeight.Bold, containerColor = Color.Transparent
color = Color.White ),
) elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Text( Box(
text = "Langkah Hari Ini", modifier = Modifier
style = MaterialTheme.typography.titleMedium, .fillMaxWidth()
color = Color.White.copy(alpha = 0.9f) .background(
) brush = Brush.verticalGradient(
colors = listOf(
Spacer(modifier = Modifier.height(16.dp)) Color(0xFF4CAF50),
Color(0xFF81C784)
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
) )
)
)
.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 // Calories Card
item { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = Color.Transparent containerColor = Color.Transparent
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background( .background(
brush = Brush.horizontalGradient( brush = Brush.horizontalGradient(
colors = listOf( colors = listOf(
Color(0xFFFF6B6B), Color(0xFFFF6B6B),
Color(0xFFFF8E53) 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Box( Surface(
modifier = Modifier shape = CircleShape,
.size(56.dp) color = MaterialTheme.colorScheme.primaryContainer,
.clip(CircleShape) modifier = Modifier.size(48.dp)
.background(Color.White.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) { ) {
Text( Box(contentAlignment = Alignment.Center) {
text = "🔥", Icon(
style = MaterialTheme.typography.headlineMedium imageVector = Icons.Default.CalendarToday,
) contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
} }
Column { Column {
Text( Text(
text = "Kalori Terbakar", text = record.date,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Color.White.copy(alpha = 0.9f) fontWeight = FontWeight.Bold
) )
Text( Text(
text = "$todayCalories kkal", text = "${record.steps} langkah • $recordCalories kkal",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant
color = Color.White
) )
} }
} }
Column( Column(
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
Text( CircularProgressIndicator(
text = "Berat: ${userWeight}kg", progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) },
style = MaterialTheme.typography.bodySmall, modifier = Modifier.size(48.dp),
color = Color.White.copy(alpha = 0.8f) strokeWidth = 4.dp
) )
Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "${String.format("%.2f", todayCalories / 7.7)} gram lemak", text = "${((record.steps.toFloat() / stepGoal.toFloat()) * 100).toInt()}%",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.labelSmall,
color = Color.White.copy(alpha = 0.8f) fontWeight = FontWeight.Bold
) )
} }
} }
@ -281,111 +502,24 @@ fun StepsScreen(
} }
} }
// Motivational Message - TAMBAH INI // Floating step indicator
item { AnimatedVisibility(
val message = when { visible = stepDetected && isTracking,
(todaySteps?.steps ?: 0) >= stepGoal -> "🎉 Luar biasa! Target tercapai!" enter = scaleIn(initialScale = 0.5f) + fadeIn(),
(todaySteps?.steps ?: 0) >= (stepGoal * 0.75) -> "💪 Hampir sampai! Tetap semangat!" exit = scaleOut(targetScale = 1.5f) + fadeOut(),
(todaySteps?.steps ?: 0) >= (stepGoal * 0.5) -> "🚶 Setengah jalan! Ayo lanjutkan!" modifier = Modifier.align(Alignment.Center)
(todaySteps?.steps ?: 0) >= (stepGoal * 0.25) -> "👟 Awal yang bagus! Terus bergerak!" ) {
else -> "🌟 Yuk mulai bergerak hari ini!" Surface(
} shape = CircleShape,
color = Color.White,
Card( shadowElevation = 8.dp,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.size(80.dp)
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
) { ) {
Text( Box(contentAlignment = Alignment.Center) {
text = message, Text(
style = MaterialTheme.typography.bodyLarge, text = "👟",
modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.displaySmall
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
)
}
} }
} }
} }
@ -400,7 +534,7 @@ fun AnimatedWalkingIcon() {
initialValue = -10f, initialValue = -10f,
targetValue = 10f, targetValue = 10f,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(500, easing = LinearEasing), animation = tween(400, easing = LinearEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
), ),
label = "rotation" label = "rotation"
@ -419,20 +553,21 @@ fun AnimatedWalkingIcon() {
@Composable @Composable
fun PulsingDot() { fun PulsingDot() {
val infiniteTransition = rememberInfiniteTransition(label = "pulse") val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val alpha by infiniteTransition.animateFloat( val scale by infiniteTransition.animateFloat(
initialValue = 1f, initialValue = 0.8f,
targetValue = 0.3f, targetValue = 1.2f,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing), animation = tween(600, easing = LinearEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
), ),
label = "alpha" label = "scale"
) )
Box( Box(
modifier = Modifier modifier = Modifier
.size(8.dp) .size(8.dp)
.scale(scale)
.clip(CircleShape) .clip(CircleShape)
.background(Color.White.copy(alpha = alpha)) .background(Color(0xFF4CAF50))
) )
} }

View File

@ -1,22 +1,22 @@
package com.example.stepdrink.viewmodel package com.example.stepdrink.viewmodel
import android.app.Application import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.stepdrink.data.local.PreferencesManager import com.example.stepdrink.data.local.PreferencesManager
import com.example.stepdrink.data.local.database.AppDatabase import com.example.stepdrink.data.local.database.AppDatabase
import com.example.stepdrink.data.local.entity.StepRecord import com.example.stepdrink.data.local.entity.StepRecord
import com.example.stepdrink.data.repository.StepRepository 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 com.example.stepdrink.util.DateUtils
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class StepViewModel(application: Application) : AndroidViewModel(application) { class StepViewModel(application: Application) : AndroidViewModel(application) {
// PERBAIKAN: Initialize repository dulu di init block
private val repository: StepRepository private val repository: StepRepository
val stepCounterManager: StepCounterManager val improvedStepManager: ImprovedStepManager
private val preferencesManager = PreferencesManager(application) private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> val dailyGoal: StateFlow<Int>
@ -24,11 +24,19 @@ class StepViewModel(application: Application) : AndroidViewModel(application) {
val todaySteps: StateFlow<StepRecord?> val todaySteps: StateFlow<StepRecord?>
val last7DaysSteps: StateFlow<List<StepRecord>> val last7DaysSteps: StateFlow<List<StepRecord>>
companion object {
private const val TAG = "StepViewModel"
}
init { init {
Log.d(TAG, "=== VIEWMODEL INIT ===")
// Initialize database & repository // Initialize database & repository
val database = AppDatabase.getDatabase(application) val database = AppDatabase.getDatabase(application)
repository = StepRepository(database.stepDao()) repository = StepRepository(database.stepDao())
stepCounterManager = StepCounterManager(application) improvedStepManager = ImprovedStepManager(application)
Log.d(TAG, "Sensor available: ${improvedStepManager.isSensorAvailable()}")
// Initialize flows // Initialize flows
dailyGoal = preferencesManager.stepGoal dailyGoal = preferencesManager.stepGoal
@ -45,16 +53,25 @@ class StepViewModel(application: Application) : AndroidViewModel(application) {
// Observe sensor dan update database // Observe sensor dan update database
viewModelScope.launch { viewModelScope.launch {
stepCounterManager.totalSteps.collect { steps -> improvedStepManager.totalSteps.collect { steps ->
Log.d(TAG, "Steps updated: $steps")
if (steps > 0) { if (steps > 0) {
updateTodaySteps(steps) updateTodaySteps(steps)
} }
} }
} }
// Log sensor status
viewModelScope.launch {
improvedStepManager.sensorStatus.collect { status ->
Log.d(TAG, "Sensor status: $status")
}
}
} }
private fun updateTodaySteps(steps: Int) { private fun updateTodaySteps(steps: Int) {
viewModelScope.launch { viewModelScope.launch {
Log.d(TAG, "Updating database: $steps steps for ${DateUtils.getCurrentDate()}")
repository.insertOrUpdateSteps(DateUtils.getCurrentDate(), steps) repository.insertOrUpdateSteps(DateUtils.getCurrentDate(), steps)
} }
} }
@ -64,22 +81,23 @@ class StepViewModel(application: Application) : AndroidViewModel(application) {
return (today.toFloat() / dailyGoal.value.toFloat()).coerceIn(0f, 1f) return (today.toFloat() / dailyGoal.value.toFloat()).coerceIn(0f, 1f)
} }
// Function untuk hitung kalori
fun calculateCalories(steps: Int, weight: Int): Int { 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() return ((steps * weight * 0.57) / 1000).toInt()
} }
// Get kalori untuk hari ini
fun getTodayCalories(): Int { fun getTodayCalories(): Int {
val steps = todaySteps.value?.steps ?: 0 val steps = todaySteps.value?.steps ?: 0
val weight = userWeight.value val weight = userWeight.value
return calculateCalories(steps, weight) return calculateCalories(steps, weight)
} }
fun getDebugInfo(): String {
return improvedStepManager.getDebugInfo()
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
stepCounterManager.stopTracking() Log.d(TAG, "=== VIEWMODEL CLEARED ===")
improvedStepManager.stopTracking()
} }
} }