1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-23 03:12:14 +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" 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" 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 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, Step::class,
Food::class Food::class
], ],
version = 2, version = 3,
autoMigrations = [ autoMigrations = [
AutoMigration(1, 2) AutoMigration(1, 2),
AutoMigration(2, 3)
], ],
exportSchema = true exportSchema = true
) )

View File

@ -3,8 +3,10 @@ package com.dzeio.openhealth.data.weight
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.dzeio.openhealth.units.ActivityLevel
import java.sql.Date import java.sql.Date
import java.text.DateFormat.getDateInstance import java.text.DateFormat.getDateInstance
import kotlin.math.roundToInt
@Entity() @Entity()
data class Weight( data class Weight(
@ -68,12 +70,63 @@ data class Weight(
/** /**
* visceral fat in it's own unit? * 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)) fun formatTimestamp(): String = getDateInstance().format(Date(timestamp))
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (!(other is Weight)) { if (other !is Weight) {
return super.equals(other) return super.equals(other)
} }
@ -87,4 +140,19 @@ data class Weight(
boneMass == other.boneMass && boneMass == other.boneMass &&
visceralFat == other.visceralFat 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) val languagesPreference = findPreference<ListPreference>(Settings.APP_LANGUAGE)
Log.d(TAG, Locale.getDefault().language) Log.d(TAG, Locale.getDefault().language)
languagesPreference?.apply { languagesPreference?.apply {

View File

@ -7,12 +7,13 @@ import com.dzeio.openhealth.Settings
import com.dzeio.openhealth.core.BaseViewModel import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.data.weight.Weight
import com.dzeio.openhealth.data.weight.WeightRepository import com.dzeio.openhealth.data.weight.WeightRepository
import com.dzeio.openhealth.units.ActivityLevel
import com.dzeio.openhealth.units.Units import com.dzeio.openhealth.units.Units
import com.dzeio.openhealth.utils.Configuration import com.dzeio.openhealth.utils.Configuration
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class WeightDialogViewModel @Inject internal constructor( class WeightDialogViewModel @Inject internal constructor(
@ -21,6 +22,10 @@ class WeightDialogViewModel @Inject internal constructor(
) : BaseViewModel() { ) : BaseViewModel() {
private val _goalWeight = settings.getFloat(Settings.WEIGHT_GOAL) 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() val goalWeight = _goalWeight.toLiveData()
@ -45,7 +50,18 @@ class WeightDialogViewModel @Inject internal constructor(
} }
suspend fun addWeight(weight: Float) { 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?) { 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:layout_height="200dp"
android:minHeight="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 <Button
android:id="@+id/goal_button" android:id="@+id/goal_button"
android:text="@string/add_goal" 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="import_complete">Import réussi, redémarrage de l\'application</string>
<string name="export_complete">Export Réussi!</string> <string name="export_complete">Export Réussi!</string>
<string name="import_export">Importer/Exporter</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> </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="import_complete">Import successful, Restarting the application</string>
<string name="export_complete">Export successful!</string> <string name="export_complete">Export successful!</string>
<string name="import_export">Import/Export</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> </resources>

View File

@ -4,20 +4,31 @@
<PreferenceCategory android:title="@string/settings_global"> <PreferenceCategory android:title="@string/settings_global">
<ListPreference <ListPreference
android:defaultValue="Male" android:defaultValue="Male"
android:entries="@array/genders" android:entries="@array/biological_sex"
android:entryValues="@array/genders" android:entryValues="@array/biological_sex"
android:key="global_gender" android:key="tmp.com.dzeio.open-health.biological_sex"
android:title="Gender" /> android:title="Biological sex" />
<ListPreference <ListPreference
android:key="com.dzeio.open-health.app.language" android:key="com.dzeio.open-health.app.language"
android:title="@string/languages" /> 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>
<PreferenceCategory android:title="Weight Settings"> <PreferenceCategory android:title="Weight Settings">
<EditTextPreference <EditTextPreference
android:key="global_height" android:key="tmp_height"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
android:singleLine="true" android:singleLine="true"
android:title="Height" /> android:title="Height" />