mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-06-12 17:19:18 +00:00
feat: Add daily Steps goal
This commit is contained in:
@ -58,7 +58,7 @@ android {
|
||||
targetSdk = sdkTarget
|
||||
|
||||
// Semantic Versioning
|
||||
versionName = "1.0.0"
|
||||
versionName = "0.1.0"
|
||||
versionCode = 1
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@ -82,6 +82,7 @@ android {
|
||||
getByName("release") {
|
||||
// Slimmer version
|
||||
isMinifyEnabled = true
|
||||
isDebuggable = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
@ -129,7 +130,7 @@ dependencies {
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0-alpha01")
|
||||
implementation("javax.inject:javax.inject:1")
|
||||
implementation("com.google.android.material:material:1.8.0-alpha03")
|
||||
implementation("com.google.android.material:material:1.8.0-beta01")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
||||
@ -173,13 +174,13 @@ dependencies {
|
||||
kapt("com.google.dagger:hilt-compiler:2.43.2")
|
||||
|
||||
// Google Fit
|
||||
implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
||||
implementation("com.google.android.gms:play-services-auth:20.4.0")
|
||||
implementation("androidx.health.connect:connect-client:1.0.0-alpha07")
|
||||
// implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
||||
// implementation("com.google.android.gms:play-services-auth:20.4.0")
|
||||
// implementation("androidx.health.connect:connect-client:1.0.0-alpha08")
|
||||
|
||||
// Samsung Health
|
||||
implementation(files("libs/samsung-health-data-1.5.0.aar"))
|
||||
implementation("com.google.code.gson:gson:2.9.1")
|
||||
// implementation(files("libs/samsung-health-data-1.5.0.aar"))
|
||||
// implementation("com.google.code.gson:gson:2.9.1")
|
||||
|
||||
// ROOM
|
||||
implementation("androidx.room:room-runtime:2.4.3")
|
||||
@ -195,6 +196,7 @@ dependencies {
|
||||
// OSS Licenses
|
||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||
|
||||
// Open Food Fact
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
|
||||
|
@ -26,4 +26,9 @@ object Settings {
|
||||
* format in which the weight the user want it to be displayed as
|
||||
*/
|
||||
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
|
||||
|
||||
/**
|
||||
* Goal number of steps each days
|
||||
*/
|
||||
const val STEPS_GOAL = "com.dzeio.open-health.steps.goal-daily"
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class StepsAdapter() : BaseAdapter<Step, LayoutItemListBinding>() {
|
||||
item: Step,
|
||||
position: Int
|
||||
) {
|
||||
holder.binding.value.text = "${item.value}steps"
|
||||
holder.binding.value.text = "${item.value} steps"
|
||||
holder.binding.datetime.text = item.formatTimestamp()
|
||||
holder.binding.edit.setOnClickListener {
|
||||
onItemClick?.invoke(item)
|
||||
|
@ -8,7 +8,7 @@ data class OFFProduct(
|
||||
@SerializedName("product_name")
|
||||
var name: String,
|
||||
|
||||
@SerializedName("serving_quantity")
|
||||
@SerializedName("serving_size")
|
||||
var serving: String,
|
||||
|
||||
@SerializedName("nutriments")
|
||||
|
@ -11,13 +11,16 @@ interface StepDao : BaseDao<Step> {
|
||||
@Query("SELECT * FROM Step ORDER BY timestamp DESC")
|
||||
fun getAll(): Flow<List<Step>>
|
||||
|
||||
@Query("SELECT * FROM Step where id = :weightId")
|
||||
@Query("SELECT * FROM Step WHERE timestamp >= :time")
|
||||
fun getAfter(time: Long): Flow<List<Step>>
|
||||
|
||||
@Query("SELECT * FROM Step WHERE id = :weightId")
|
||||
fun getOne(weightId: Long): Flow<Step?>
|
||||
|
||||
@Query("Select count(*) from Step")
|
||||
@Query("SELECT count(*) FROM Step")
|
||||
fun getCount(): Flow<Int>
|
||||
|
||||
@Query("Select * FROM Step ORDER BY timestamp DESC LIMIT 1")
|
||||
@Query("SELECT * FROM Step ORDER BY timestamp DESC LIMIT 1")
|
||||
fun last(): Flow<Step?>
|
||||
|
||||
@Query("DELETE FROM Step where source = :source")
|
||||
|
@ -2,7 +2,8 @@ package com.dzeio.openhealth.data.step
|
||||
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -23,7 +24,13 @@ class StepRepository @Inject constructor(
|
||||
suspend fun deleteFromSource(value: String) = stepDao.deleteFromSource(value)
|
||||
|
||||
suspend fun todaySteps(): Int {
|
||||
val steps = getSteps().firstOrNull()
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
val steps = stepDao.getAfter(cal.timeInMillis).firstOrNull()
|
||||
if (steps == null) {
|
||||
return 0
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class AboutFragment : BaseStaticFragment<FragmentAboutBinding>() {
|
||||
resources.getString(R.string.version_number, BuildConfig.VERSION_NAME)
|
||||
|
||||
binding.contactUs.setOnClickListener {
|
||||
openLink("mailto:context.openhealth@dze.io")
|
||||
openLink("mailto:contact.openhealth@dze.io")
|
||||
}
|
||||
|
||||
binding.github.setOnClickListener {
|
||||
|
@ -92,18 +92,29 @@ class BrowseFragment :
|
||||
}
|
||||
|
||||
viewModel.steps.observe(viewLifecycleOwner) {
|
||||
binding.stepsText.setText("$it of xxx steps")
|
||||
updateStepsText(it, viewModel.stepsGoal.value)
|
||||
}
|
||||
|
||||
viewModel.stepsGoal.observe(viewLifecycleOwner) {
|
||||
updateStepsText(viewModel.steps.value, it)
|
||||
}
|
||||
|
||||
viewModel.weight.observe(viewLifecycleOwner) {
|
||||
binding.weightText.setText(
|
||||
String.format(
|
||||
resources.getString(R.string.weight_current),
|
||||
it,
|
||||
resources.getString(R.string.unit_mass_kilogram_unit)
|
||||
String.format(resources.getString(R.string.unit_mass_kilogram_unit), it)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateStepsText(numberOfSteps: Int?, goal: Int?) {
|
||||
var text = "${numberOfSteps ?: 0} steps"
|
||||
if (goal != null) {
|
||||
text = "${numberOfSteps ?: 0} of $goal steps"
|
||||
}
|
||||
binding.stepsText.setText(text)
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ package com.dzeio.openhealth.ui.browse
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dzeio.openhealth.Settings
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.step.StepRepository
|
||||
import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@ -14,12 +16,16 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class BrowseViewModel @Inject internal constructor(
|
||||
stepRepository: StepRepository,
|
||||
weightRepository: WeightRepository
|
||||
weightRepository: WeightRepository,
|
||||
private val config: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _steps = MutableLiveData(0)
|
||||
val steps: LiveData<Int> = _steps
|
||||
|
||||
private val _stepsGoal: MutableLiveData<Int?> = MutableLiveData()
|
||||
val stepsGoal: LiveData<Int?> = _stepsGoal
|
||||
|
||||
private val _weight = MutableLiveData(0f)
|
||||
val weight: LiveData<Float> = _weight
|
||||
|
||||
@ -36,5 +42,9 @@ class BrowseViewModel @Inject internal constructor(
|
||||
_weight.postValue(it.weight)
|
||||
}
|
||||
}
|
||||
|
||||
this._stepsGoal.postValue(
|
||||
config.getInt(Settings.STEPS_GOAL).value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -91,5 +91,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
return@setOnPreferenceChangeListener true
|
||||
}
|
||||
|
||||
val stepsGoalPreference = findPreference<EditTextPreference>(Settings.STEPS_GOAL)
|
||||
stepsGoalPreference.apply {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,12 +64,16 @@ class StepsHomeFragment :
|
||||
)
|
||||
}
|
||||
|
||||
val errorColor = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorError
|
||||
)
|
||||
|
||||
chart.apply {
|
||||
series = arrayListOf(serie)
|
||||
// debug = true
|
||||
|
||||
yAxis.apply {
|
||||
setYMax(500f)
|
||||
textLabel.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimaryContainer
|
||||
@ -78,14 +82,15 @@ class StepsHomeFragment :
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimaryContainer
|
||||
)
|
||||
goalLinePaint.color = errorColor
|
||||
//
|
||||
onValueFormat = { value -> "${value.toInt()}" }
|
||||
}
|
||||
|
||||
xAxis.apply {
|
||||
increment = 3600000.0
|
||||
increment = 86400000.0
|
||||
// displayCount = 168
|
||||
displayCount = 10
|
||||
// displayCount = 10
|
||||
textPaint.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimaryContainer
|
||||
@ -103,6 +108,11 @@ class StepsHomeFragment :
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.goal.observe(viewLifecycleOwner) {
|
||||
chart.yAxis.setGoalLine(it?.toFloat())
|
||||
chart.refresh()
|
||||
}
|
||||
|
||||
viewModel.items.observe(viewLifecycleOwner) { list ->
|
||||
adapter.set(list)
|
||||
|
||||
@ -110,12 +120,7 @@ class StepsHomeFragment :
|
||||
return@observe
|
||||
}
|
||||
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
|
||||
// chart.animation.enabled = false
|
||||
// chart.animation.refreshRate = 60
|
||||
@ -125,11 +130,33 @@ class StepsHomeFragment :
|
||||
|
||||
// chart.xAxis.labels.size = 32f
|
||||
|
||||
serie.entries = list.reversed().map {
|
||||
return@map Entry(it.timestamp.toDouble(), it.value.toFloat())
|
||||
} as ArrayList<Entry>
|
||||
val entries: HashMap<Long, Entry> = HashMap()
|
||||
|
||||
chart.xAxis.x = serie.entries.first().x
|
||||
list.forEach {
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
cal.timeInMillis = it.timestamp
|
||||
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.AM_PM, Calendar.AM)
|
||||
val ts = cal.timeInMillis
|
||||
if (!entries.containsKey(ts)) {
|
||||
entries[ts] = Entry((ts).toDouble(), 0F, errorColor)
|
||||
}
|
||||
|
||||
entries[ts]!!.y += it.value.toFloat()
|
||||
|
||||
if (viewModel.goal.value != null) {
|
||||
if (entries[ts]!!.y > viewModel.goal.value!!) {
|
||||
entries[ts]!!.color = null
|
||||
}
|
||||
} else {
|
||||
entries[ts]!!.color = null
|
||||
}
|
||||
}
|
||||
|
||||
serie.entries = ArrayList(entries.values)
|
||||
|
||||
chart.xAxis.x = chart.xAxis.getXMax()
|
||||
|
||||
|
||||
chart.refresh()
|
||||
|
@ -1,10 +1,13 @@
|
||||
package com.dzeio.openhealth.ui.steps
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dzeio.openhealth.Settings
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.step.Step
|
||||
import com.dzeio.openhealth.data.step.StepRepository
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@ -12,15 +15,23 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StepsHomeViewModel@Inject internal constructor(
|
||||
private val stepRepository: StepRepository
|
||||
private val stepRepository: StepRepository,
|
||||
private val config: Configuration
|
||||
) : BaseViewModel() {
|
||||
val items: MutableLiveData<List<Step>> = MutableLiveData()
|
||||
|
||||
private val _goal: MutableLiveData<Int?> = MutableLiveData()
|
||||
val goal: LiveData<Int?> = _goal
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launch {
|
||||
stepRepository.getSteps().collectLatest {
|
||||
items.postValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
this._goal.postValue(
|
||||
config.getInt(Settings.STEPS_GOAL).value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
package com.dzeio.openhealth.utils.fields
|
||||
|
||||
import android.content.Context
|
||||
import android.text.InputType
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
import androidx.preference.EditTextPreference
|
||||
|
||||
|
||||
class IntEditTextPreference : EditTextPreference, EditTextPreference.OnBindEditTextListener {
|
||||
|
||||
private var txt: String? = null
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int,
|
||||
defStyleRes: Int
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setOnBindEditTextListener(this)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
) {
|
||||
setOnBindEditTextListener(this)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
setOnBindEditTextListener(this)
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
setOnBindEditTextListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the text to the current data storage.
|
||||
*
|
||||
* @param text The text to save
|
||||
*/
|
||||
override fun setText(text: String?) {
|
||||
|
||||
val wasBlocking = shouldDisableDependents()
|
||||
val pouet = Integer.parseInt(text.toString())
|
||||
this.txt = text
|
||||
pouet::class.simpleName?.let { Log.d("pouet2", it) }
|
||||
persistInt(pouet)
|
||||
val isBlocking = shouldDisableDependents()
|
||||
if (isBlocking != wasBlocking) {
|
||||
notifyDependencyChange(isBlocking)
|
||||
}
|
||||
notifyChanged()
|
||||
}
|
||||
|
||||
override fun getText(): String? {
|
||||
return this.txt
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(defaultValue: Any?) {
|
||||
var value: Int
|
||||
if (defaultValue != null) {
|
||||
val strDefaultValue = defaultValue as String
|
||||
val defaultIntValue = strDefaultValue.toInt()
|
||||
value = getPersistedInt(defaultIntValue)
|
||||
} else {
|
||||
try {
|
||||
value = getPersistedInt(0)
|
||||
} catch (e: ClassCastException) {
|
||||
value = 0
|
||||
}
|
||||
}
|
||||
text = value.toString()
|
||||
}
|
||||
|
||||
override fun shouldDisableDependents(): Boolean {
|
||||
return TextUtils.isEmpty(text) || super.shouldDisableDependents()
|
||||
}
|
||||
|
||||
override fun onBindEditText(editText: EditText) {
|
||||
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
}
|
||||
}
|
@ -13,11 +13,11 @@
|
||||
<!-- Units -->
|
||||
<string name="unit_mass_kilogram_name_singular">Kilogram</string>
|
||||
<string name="unit_mass_kilogram_name_plural">Kilograms</string>
|
||||
<string name="unit_mass_kilogram_unit" translatable="false">kg</string>
|
||||
<string name="unit_mass_kilogram_unit" translatable="false">%1$skg</string>
|
||||
|
||||
<string name="unit_mass_pound_name_singular">Pound</string>
|
||||
<string name="unit_mass_pound_name_plural">Pounds</string>
|
||||
<string name="unit_mass_pound_unit" translatable="false">lbs</string>
|
||||
<string name="unit_mass_pound_unit" translatable="false">%1$slbs</string>
|
||||
|
||||
<string name="unit_volume_milliliter_name_singular">Milliliter</string>
|
||||
<string name="unit_volume_milliliter_name_plural">Milliliters</string>
|
||||
@ -52,7 +52,7 @@
|
||||
<string name="permission_declined">You declined a permission, you can\'t use this extension unless you enable it manually</string>
|
||||
<string name="edit_daily_goal">Modifiy daily goal</string>
|
||||
<string name="menu_steps">Steps</string>
|
||||
<string name="weight_current">Current weight: %1$s%2$s</string>
|
||||
<string name="weight_current">Current weight: %1$s</string>
|
||||
|
||||
|
||||
<!-- Error Activity Translations -->
|
||||
|
@ -13,6 +13,7 @@
|
||||
android:key="com.dzeio.open-health.app.language"
|
||||
android:title="@string/languages" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Weight Settings">
|
||||
|
||||
<EditTextPreference
|
||||
@ -32,9 +33,8 @@
|
||||
android:entryValues="@array/mass_units"
|
||||
android:key="com.dzeio.open-health.unit.mass"
|
||||
android:title="Mass Unit" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Water Settings">
|
||||
|
||||
<SwitchPreference
|
||||
@ -55,4 +55,14 @@
|
||||
android:inputType="number"
|
||||
android:title="Daily Water intake" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Steps settings">
|
||||
<com.dzeio.openhealth.utils.fields.IntEditTextPreference
|
||||
android:key="com.dzeio.open-health.steps.goal-daily"
|
||||
android:title="Number of steps each steps"
|
||||
android:selectAllOnFocus="true"
|
||||
android:singleLine="true"
|
||||
android:inputType="number"
|
||||
/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
Reference in New Issue
Block a user