diff --git a/app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json b/app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json new file mode 100644 index 0000000..516319d --- /dev/null +++ b/app/schemas/com.dzeio.openhealth.data.AppDatabase/3.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/Settings.kt b/app/src/main/java/com/dzeio/openhealth/Settings.kt index afde599..7b01b8b 100644 --- a/app/src/main/java/com/dzeio/openhealth/Settings.kt +++ b/app/src/main/java/com/dzeio/openhealth/Settings.kt @@ -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" } diff --git a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt index 577ac41..804e1e6 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt @@ -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 ) diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt index b00c9a7..bd2ce05 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/weight/Weight.kt @@ -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 + } } diff --git a/app/src/main/java/com/dzeio/openhealth/ui/settings/SettingsFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/settings/SettingsFragment.kt index 640e794..07fda30 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/settings/SettingsFragment.kt @@ -72,6 +72,58 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + val height = findPreference("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("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("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(Settings.APP_LANGUAGE) Log.d(TAG, Locale.getDefault().language) languagesPreference?.apply { diff --git a/app/src/main/java/com/dzeio/openhealth/ui/weight/WeightDialogViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/weight/WeightDialogViewModel.kt index 6738716..dd6994e 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/weight/WeightDialogViewModel.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/weight/WeightDialogViewModel.kt @@ -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?) { diff --git a/app/src/main/java/com/dzeio/openhealth/units/ActivityLevel.kt b/app/src/main/java/com/dzeio/openhealth/units/ActivityLevel.kt new file mode 100644 index 0000000..4bc7e76 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/units/ActivityLevel.kt @@ -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) +} diff --git a/app/src/main/res/layout/fragment_list_weight.xml b/app/src/main/res/layout/fragment_list_weight.xml index 86f791a..c565bd0 100644 --- a/app/src/main/res/layout/fragment_list_weight.xml +++ b/app/src/main/res/layout/fragment_list_weight.xml @@ -104,6 +104,14 @@ android:layout_height="200dp" android:minHeight="200dp" /> + +