1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-09-01 12:23:15 +02:00

Backfill derived values on app start if needed

This commit is contained in:
oliexdev
2025-08-20 16:08:35 +02:00
parent 4e5c24f7fe
commit 3e966fbb7c
3 changed files with 94 additions and 0 deletions

View File

@@ -17,9 +17,11 @@
*/
package com.health.openscale.ui.screen
import android.R.attr.duration
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import androidx.annotation.IntegerRes
import androidx.annotation.StringRes
import androidx.compose.material3.SnackbarDuration
import androidx.compose.runtime.Composable
@@ -68,6 +70,7 @@ import java.time.Instant
import java.time.ZoneId
import java.util.Calendar
import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.roundToInt
private const val TAG = "SharedViewModel"
@@ -421,9 +424,89 @@ class SharedViewModel(
LogManager.v(TAG, "lastMeasurementOfSelectedUser flow initialized. (Derived Data Flow)")
}
private val didRunDerivedBackfill = AtomicBoolean(false)
private val _isBaseDataLoading = MutableStateFlow(false)
val isBaseDataLoading: StateFlow<Boolean> = _isBaseDataLoading.asStateFlow()
/**
* Ensures that all derived measurement values (e.g., BMI, LBM, WHR) are present
* for all users in the database.
*
* This method runs only once per app session (guarded by [didRunDerivedBackfill]).
* It iterates over all users, checks if at least one valid BMI value exists, and if not,
* triggers a recalculation of all derived values for every measurement of that user.
*
* Typical use case:
* - After database migration when derived values were not persisted in older versions.
* - After restoring a backup where derived values may be missing.
*/
fun maybeBackfillDerivedValues() {
if (!didRunDerivedBackfill.compareAndSet(false, true)) return
viewModelScope.launch(Dispatchers.IO) {
try {
val types = databaseRepository.getAllMeasurementTypes().first()
val bmiType = types.firstOrNull { it.key == MeasurementTypeKey.BMI } ?: run {
LogManager.w(TAG, "Backfill skip: BMI type not found.")
return@launch
}
val allUsers = databaseRepository.getAllUsers().first()
if (allUsers.isEmpty()) {
LogManager.d(TAG, "Backfill skip: no users.")
return@launch
}
var totalMeasurements = 0
var ok = 0
var usersAffected = 0
allUsers.forEach { user ->
val all = databaseRepository.getMeasurementsWithValuesForUser(user.id).first()
if (all.isEmpty()) {
LogManager.d(TAG, "Backfill skip: no measurements for userId=${user.id}.")
return@forEach
}
val hasAnyBmi = all.any { mwv ->
mwv.values.any { v ->
v.type.id == bmiType.id && (v.value.floatValue ?: 0f) > 0f
}
}
if (hasAnyBmi) {
LogManager.d(TAG, "Backfill not needed for userId=${user.id}: at least one BMI>0 exists.")
return@forEach
}
usersAffected++
totalMeasurements += all.size
LogManager.i(TAG, "No BMI for userId=${user.id} -> recalculating derived values for all ${all.size} measurements…")
showSnackbar(messageResId = R.string.derived_backfill_start, duration = SnackbarDuration.Short)
all.forEach { mwv ->
try {
databaseRepository.recalculateDerivedValuesForMeasurement(mwv.measurement.id)
ok++
} catch (e: Exception) {
LogManager.e(TAG, "Recalc failed for measurementId=${mwv.measurement.id}", e)
}
}
}
if (usersAffected == 0) {
LogManager.i(TAG, "Derived backfill not needed for any user.")
} else {
LogManager.i(TAG, "Derived backfill done: $ok/$totalMeasurements measurements processed across $usersAffected users.")
showSnackbar(messageResId = R.string.derived_backfill_done, duration = SnackbarDuration.Short)
}
} catch (e: Exception) {
LogManager.e(TAG, "Derived backfill fatal error", e)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
val enrichedMeasurementsFlow: StateFlow<List<EnrichedMeasurement>> =
allMeasurementsForSelectedUser.combine(measurementTypes) { measurements, globalTypes ->
@@ -786,6 +869,13 @@ class SharedViewModel(
}
}
// Ensure derived values are backfilled once after users and types are loaded
viewModelScope.launch {
allUsers.first()
measurementTypes.first()
maybeBackfillDerivedValues()
}
LogManager.i(TAG, "ViewModel initialization complete. (Lifecycle Event)")
}

View File

@@ -17,6 +17,8 @@
<string name="content_desc_open_menu">Menü öffnen</string>
<string name="content_desc_back">Zurück</string>
<string name="content_desc_selected">Ausgewählt</string>
<string name="derived_backfill_start">Berechne abgeleitete Werte …</string>
<string name="derived_backfill_done">Abgeleitete Werte wurden erfolgreich aktualisiert.</string>
<!-- Routen- & Bildschirmtitel -->
<string name="route_title_overview">Übersicht</string>

View File

@@ -18,6 +18,8 @@
<string name="content_desc_open_menu">Open menu</string>
<string name="content_desc_back">Back</string>
<string name="content_desc_selected">Selected</string>
<string name="derived_backfill_start">Calculating derived values …</string>
<string name="derived_backfill_done">Derived values successfully updated.</string>
<!-- Route & Screen Titles -->
<string name="route_title_overview">Overview</string>