sensor update
This commit is contained in:
parent
a23005280b
commit
acc4e48a1a
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -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
13
.idea/deviceManager.xml
generated
Normal 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>
|
||||||
@ -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"
|
||||||
|
|||||||
259
app/src/main/java/sensor/HybridStepManager.kt
Normal file
259
app/src/main/java/sensor/HybridStepManager.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,6 +173,7 @@ fun StepsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -104,8 +181,36 @@ fun StepsScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
// Debug Info Card (collapsible)
|
||||||
|
if (showDebugInfo) {
|
||||||
item {
|
item {
|
||||||
// Main Steps Card dengan Gradient
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
// Main Steps Card
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
@ -130,8 +235,7 @@ fun StepsScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// Animated Icon
|
if (isTracking && isWalking) {
|
||||||
if (isTracking) {
|
|
||||||
AnimatedWalkingIcon()
|
AnimatedWalkingIcon()
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
@ -144,8 +248,9 @@ fun StepsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Display sensor steps in real-time when tracking
|
||||||
Text(
|
Text(
|
||||||
text = "${todaySteps?.steps ?: 0}",
|
text = if (isTracking) "$sensorSteps" else "${todaySteps?.steps ?: 0}",
|
||||||
style = MaterialTheme.typography.displayLarge,
|
style = MaterialTheme.typography.displayLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
@ -161,7 +266,8 @@ fun StepsScreen(
|
|||||||
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = {
|
progress = {
|
||||||
(todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat()
|
val steps = if (isTracking) sensorSteps else (todaySteps?.steps ?: 0)
|
||||||
|
steps.toFloat() / stepGoal.toFloat()
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -190,11 +296,16 @@ fun StepsScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
PulsingDot()
|
Icon(
|
||||||
|
imageVector = Icons.Default.Sensors,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
"Sedang Tracking: $sensorSteps langkah",
|
"Tracking Aktif • Real-time",
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -205,7 +316,7 @@ fun StepsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TAMBAH INI - Calories Card
|
// Calories Card
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@ -271,7 +382,7 @@ fun StepsScreen(
|
|||||||
color = Color.White.copy(alpha = 0.8f)
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "≈ ${String.format("%.2f", todayCalories / 7.7)} gram lemak",
|
text = "≈ ${String.format("%.1f", todayCalories / 7.7)}g lemak",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color.White.copy(alpha = 0.8f)
|
color = Color.White.copy(alpha = 0.8f)
|
||||||
)
|
)
|
||||||
@ -281,7 +392,7 @@ fun StepsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Motivational Message - TAMBAH INI
|
// Rest of the items (Motivational message, history, etc.)
|
||||||
item {
|
item {
|
||||||
val message = when {
|
val message = when {
|
||||||
(todaySteps?.steps ?: 0) >= stepGoal -> "🎉 Luar biasa! Target tercapai!"
|
(todaySteps?.steps ?: 0) >= stepGoal -> "🎉 Luar biasa! Target tercapai!"
|
||||||
@ -390,6 +501,29 @@ fun StepsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = "👟",
|
||||||
|
style = MaterialTheme.typography.displaySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user