1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-22 19:02:16 +00:00

feat: Add BMR and TDEE and automate them with the BMI (#154)

This commit is contained in:
Florian Bouillon 2023-02-28 15:45:27 +01:00 committed by GitHub
parent 1aa7f2d89e
commit cd537470ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 523 additions and 18 deletions

View File

@ -0,0 +1,286 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "5558211d53c890889333e6b1559952c7",
"entities": [
{
"tableName": "Weight",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `weight` REAL NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL, `bmi` REAL, `totalBodyWater` REAL, `muscles` REAL, `leanBodyMass` REAL, `bodyFat` REAL, `boneMass` REAL, `visceralFat` REAL, `basalMetabolicRate` INTEGER, `totalDailyEnergyExpendure` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bmi",
"columnName": "bmi",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "totalBodyWater",
"columnName": "totalBodyWater",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "muscles",
"columnName": "muscles",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "leanBodyMass",
"columnName": "leanBodyMass",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "bodyFat",
"columnName": "bodyFat",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "boneMass",
"columnName": "boneMass",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "visceralFat",
"columnName": "visceralFat",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "basalMetabolicRate",
"columnName": "basalMetabolicRate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "totalDailyEnergyExpendure",
"columnName": "totalDailyEnergyExpendure",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Weight_timestamp",
"unique": false,
"columnNames": [
"timestamp"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Weight_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
}
],
"foreignKeys": []
},
{
"tableName": "Water",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Water_timestamp",
"unique": false,
"columnNames": [
"timestamp"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Water_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
}
],
"foreignKeys": []
},
{
"tableName": "Step",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Step_timestamp",
"unique": false,
"columnNames": [
"timestamp"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Step_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
}
],
"foreignKeys": []
},
{
"tableName": "Food",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `serving` TEXT NOT NULL, `quantity` REAL NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `image` TEXT, `timestamp` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serving",
"columnName": "serving",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "proteins",
"columnName": "proteins",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "carbohydrates",
"columnName": "carbohydrates",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "fat",
"columnName": "fat",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "energy",
"columnName": "energy",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5558211d53c890889333e6b1559952c7')"
]
}
}

View File

@ -35,6 +35,40 @@ object Settings {
*/
const val STEPS_GOAL = "com.dzeio.open-health.steps.goal-daily"
/**
* The water intake size for the quick add
*/
const val WATER_INTAKE_SIZE = "com.dzeio.open-health.water.size"
/**
* the default value for the setting above
*/
const val WATER_INTAKE_SIZE_DEFAULT = 250
/**
* the user Height in CM
*/
const val USER_HEIGHT = "com.dzeio.open-health.height"
/**
* the user birthday as an ISO8601 date
*/
const val USER_BIRTHDAY = "com.dzeio.open-health.birthday"
/**
* the User age
*/
const val USER_AGE = "com.dzeio.open-health.age"
/**
* the user biologicial age (0 = female, 1 = male)
*/
const val USER_BIOLOGICAL_SEX = "com.dzeio.open-health.biological_sex"
/**
* the user activity level
*
* @see com.dzeio.openhealth.units.ActivityLevel
*/
const val USER_ACTIVITY_LEVEL = "com.dzeio.open-health.activity_level"
}

View File

@ -31,9 +31,10 @@ import java.util.zip.ZipInputStream
Step::class,
Food::class
],
version = 2,
version = 3,
autoMigrations = [
AutoMigration(1, 2)
AutoMigration(1, 2),
AutoMigration(2, 3)
],
exportSchema = true
)

View File

@ -3,8 +3,10 @@ package com.dzeio.openhealth.data.weight
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.dzeio.openhealth.units.ActivityLevel
import java.sql.Date
import java.text.DateFormat.getDateInstance
import kotlin.math.roundToInt
@Entity()
data class Weight(
@ -68,12 +70,63 @@ data class Weight(
/**
* visceral fat in it's own unit?
*/
var visceralFat: Float? = null
var visceralFat: Float? = null,
/**
* the BMR rate
*/
var basalMetabolicRate: Int? = null,
var totalDailyEnergyExpendure: Int? = null
) {
/**
* Run estimations to calculate & set different elements if they are not set
*
* @param height the height in `cm`
* @param age the user's age
* @param biologicalSex the user's biological sex (female = 0, male = 1)
* @param activityLevel the level of activity of the user
*/
fun setItemsWithEstimation(
height: Int? = null,
age: Int? = null,
biologicalSex: Int? = null,
activityLevel: ActivityLevel? = null
) {
if (bmi == null && height != null) {
val tmpHeight = (height / 100f)
bmi = weight / (tmpHeight * tmpHeight)
}
if (totalBodyWater == null && height != null) {
// female = 0, male = 1
totalBodyWater = (
if (biologicalSex == 0) {
((0.34454 * height) + (0.183809 * weight) - 35.270121)
} else {
((0.194786 * height) + (0.296785 * weight) - 14.012934)
}
).toFloat()
}
if (basalMetabolicRate == null && height != null && age != null) {
basalMetabolicRate = (
if (biologicalSex == 0) {
10 * weight + 6.25 * height - 5 * age - 161
} else {
10 * weight + 6.25 * height - 5 * age + 5
}
).toInt()
}
if (totalDailyEnergyExpendure == null && activityLevel != null && basalMetabolicRate != null) {
totalDailyEnergyExpendure = (basalMetabolicRate!! * activityLevel.modifier).roundToInt()
}
}
fun formatTimestamp(): String = getDateInstance().format(Date(timestamp))
override fun equals(other: Any?): Boolean {
if (!(other is Weight)) {
if (other !is Weight) {
return super.equals(other)
}
@ -87,4 +140,19 @@ data class Weight(
boneMass == other.boneMass &&
visceralFat == other.visceralFat
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + weight.hashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + source.hashCode()
result = 31 * result + (bmi?.hashCode() ?: 0)
result = 31 * result + (totalBodyWater?.hashCode() ?: 0)
result = 31 * result + (muscles?.hashCode() ?: 0)
result = 31 * result + (leanBodyMass?.hashCode() ?: 0)
result = 31 * result + (bodyFat?.hashCode() ?: 0)
result = 31 * result + (boneMass?.hashCode() ?: 0)
result = 31 * result + (visceralFat?.hashCode() ?: 0)
return result
}
}

View File

@ -72,6 +72,58 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
val height = findPreference<EditTextPreference>("tmp_height")
height?.apply {
setOnBindEditTextListener {
it.setSelectAllOnFocus(true)
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
}
val value = config.getInt(Settings.USER_HEIGHT)
setOnPreferenceClickListener {
text = value.value?.toString()
return@setOnPreferenceClickListener true
}
setOnPreferenceChangeListener { _, newValue ->
value.value = (newValue as String).toInt()
return@setOnPreferenceChangeListener false
}
}
val activityPreference = findPreference<ListPreference>("tmp.com.dzeio.open-health.activitylevel")
activityPreference?.apply {
val value = config.getInt(Settings.USER_ACTIVITY_LEVEL)
setOnPreferenceClickListener {
if (value.value != null) {
setValueIndex(value.value!!)
}
return@setOnPreferenceClickListener true
}
setOnPreferenceChangeListener { _, newValue ->
value.value = findIndexOfValue(newValue.toString())
return@setOnPreferenceChangeListener false
}
}
val biologicalSexPreference = findPreference<ListPreference>("tmp.com.dzeio.open-health.biological_sex")
biologicalSexPreference?.apply {
val value = config.getInt(Settings.USER_BIOLOGICAL_SEX)
setOnPreferenceClickListener {
if (value.value != null) {
setValueIndex(value.value!!)
}
return@setOnPreferenceClickListener true
}
setOnPreferenceChangeListener { _, newValue ->
value.value = findIndexOfValue(newValue.toString())
return@setOnPreferenceChangeListener false
}
}
val languagesPreference = findPreference<ListPreference>(Settings.APP_LANGUAGE)
Log.d(TAG, Locale.getDefault().language)
languagesPreference?.apply {

View File

@ -7,12 +7,13 @@ import com.dzeio.openhealth.Settings
import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.weight.Weight
import com.dzeio.openhealth.data.weight.WeightRepository
import com.dzeio.openhealth.units.ActivityLevel
import com.dzeio.openhealth.units.Units
import com.dzeio.openhealth.utils.Configuration
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class WeightDialogViewModel @Inject internal constructor(
@ -21,6 +22,10 @@ class WeightDialogViewModel @Inject internal constructor(
) : BaseViewModel() {
private val _goalWeight = settings.getFloat(Settings.WEIGHT_GOAL)
private val age = settings.getInt(Settings.USER_AGE)
private val activityLevel = settings.getInt(Settings.USER_ACTIVITY_LEVEL)
private val biologicalSex = settings.getInt(Settings.USER_BIOLOGICAL_SEX)
private val height = settings.getInt(Settings.USER_HEIGHT)
val goalWeight = _goalWeight.toLiveData()
@ -45,7 +50,18 @@ class WeightDialogViewModel @Inject internal constructor(
}
suspend fun addWeight(weight: Float) {
weightRepository.addWeight(Weight(weight = weight / format.modifier))
weightRepository.addWeight(
Weight(weight = weight / format.modifier).apply {
if (height.value != null) {
setItemsWithEstimation(
height.value!!,
age.value,
biologicalSex.value,
if (activityLevel.value != null) ActivityLevel.values()[activityLevel.value!!] else null
)
}
}
)
}
fun setWeightGoal(value: Float?) {

View File

@ -0,0 +1,10 @@
package com.dzeio.openhealth.units
enum class ActivityLevel(val modifier: Float) {
BEDRIDDEN(1f),
SEDENTARY(1.2f),
LIGHTLY_ACTIVE(1.375f),
MODERATELY_ACTIVE(1.55f),
VERY_ACTIVE(1.725f),
EXTREMELY_ACTIVE(1.9f)
}

View File

@ -104,6 +104,14 @@
android:layout_height="200dp"
android:minHeight="200dp" />
<TextView
android:id="@+id/goal_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="16dp"
/>
<Button
android:id="@+id/goal_button"
android:text="@string/add_goal"

View File

@ -64,4 +64,17 @@
<string name="import_complete">Import réussi, redémarrage de l\'application</string>
<string name="export_complete">Export Réussi!</string>
<string name="import_export">Importer/Exporter</string>
<string name="weight_item">Date: %1$s\nBMI: %2$.2f\nEau corporelle: %3$.2f\nMuscles: %4$.2f\nMasse maigre: %5$.2f\nMasse grasse: %6$.2f\nMasse osseuse: %7$.2f\nGraisse viscérale: %8$.2f\nMétabolisme basal: %9$d\nDépense énergétique quotidienne totale: %10$d</string>
<string-array name="activity_levels">
<item>Cloué au lit</item>
<item>Sédentaire</item>
<item>Légèrement actif</item>
<item>Modérément actif</item>
<item>Très actif</item>
<item>Extremement active</item>
</string-array>
<string-array name="biological_sex">
<item>Femme</item>
<item>Homme</item>
</string-array>
</resources>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="genders">
<item>Male</item>
<item>Female</item>
</string-array>
</resources>

View File

@ -77,4 +77,17 @@
<string name="import_complete">Import successful, Restarting the application</string>
<string name="export_complete">Export successful!</string>
<string name="import_export">Import/Export</string>
<string name="weight_item">Date: %1$s\nBMI: %2$.2f\nBody water: %3$.2f\nMuscles: %4$.2f\nLean body mass: %5$.2f\nBody fat: %6$.2f\nBone mass: %7$.2f\nVisceral fat: %8$.2f\nBasal metabolic rate: %9$d\nTotal daily energy expendure: %10$d\n</string>
<string-array name="activity_levels">
<item>Bedridden</item>
<item>Sedentary</item>
<item>Lightly active</item>
<item>Moderately active</item>
<item>Very active</item>
<item>Extremely active</item>
</string-array>
<string-array name="biological_sex">
<item>Female</item>
<item>Male</item>
</string-array>
</resources>

View File

@ -4,20 +4,31 @@
<PreferenceCategory android:title="@string/settings_global">
<ListPreference
android:defaultValue="Male"
android:entries="@array/genders"
android:entryValues="@array/genders"
android:key="global_gender"
android:title="Gender" />
android:entries="@array/biological_sex"
android:entryValues="@array/biological_sex"
android:key="tmp.com.dzeio.open-health.biological_sex"
android:title="Biological sex" />
<ListPreference
android:key="com.dzeio.open-health.app.language"
android:title="@string/languages" />
<com.dzeio.openhealth.utils.fields.IntEditTextPreference
android:key="com.dzeio.open-health.age"
android:title="Age" />
<ListPreference
android:key="tmp.com.dzeio.open-health.activitylevel"
android:entries="@array/activity_levels"
android:entryValues="@array/activity_levels"
android:title="Activity Level" />
</PreferenceCategory>
<PreferenceCategory android:title="Weight Settings">
<EditTextPreference
android:key="global_height"
android:key="tmp_height"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="Height" />