9 Commits

Author SHA1 Message Date
d76f5cf2ce MainActivity: set unit for bottle content 2026-03-10 23:14:58 +01:00
557a9ab69a DateUtils: make duration rounded up to minutes 2026-03-06 12:33:57 +01:00
620e20aa2e MainActivity: inline nested function for dynamic menu 2026-03-06 12:33:57 +01:00
8687d62bac MainActivity: generate dynamic menu from last two weeks 2026-03-06 12:33:57 +01:00
3ae68ffa7b LunaEvent: rework sleep event
Make the UI more flexible and
slightly easier to understand.
2026-03-06 12:33:53 +01:00
dd4be8dcd7 MainActivity: rename datepicker 2026-02-19 16:41:16 +01:00
cb91922e2a layout: replace menu icon with utf8 character
The three dot menu icosn looks odd when stretched
due to the dynamic menu feature. Thus replace it
with the hamburger menu character that looks better
when scaled.
2026-02-19 16:41:16 +01:00
24f48cb533 MainActivity: sort events before saving
Also replace notifyItemInserted since it does not
call the adapter to redraw the row striping when
a new event is added.
2026-02-19 16:41:16 +01:00
08022541f1 StatisticsActivity: rework all statistics
Improve the overall code.
2026-02-19 16:41:12 +01:00
7 changed files with 117 additions and 152 deletions

View File

@@ -162,7 +162,9 @@ class MainActivity : AppCompatActivity() {
compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal }) compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal })
).filter { it != LunaEvent.Type.UNKNOWN } ).filter { it != LunaEvent.Type.UNKNOWN }
fun setupMenu(maxButtonCount: Int, sortedEventTypes: List<LunaEvent.Type>): Int { val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val row1 = findViewById<View>(R.id.linear_layout_row1) val row1 = findViewById<View>(R.id.linear_layout_row1)
val row1Button1 = findViewById<TextView>(R.id.button1_row1) val row1Button1 = findViewById<TextView>(R.id.button1_row1)
val row1Button2 = findViewById<TextView>(R.id.button2_row1) val row1Button2 = findViewById<TextView>(R.id.button2_row1)
@@ -188,7 +190,7 @@ class MainActivity : AppCompatActivity() {
fun show(vararg tvs: TextView) { fun show(vararg tvs: TextView) {
for (tv in tvs) { for (tv in tvs) {
val type = sortedEventTypes[showCounter] val type = eventTypesSorted[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type) tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) } tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE tv.visibility = View.VISIBLE
@@ -207,15 +209,8 @@ class MainActivity : AppCompatActivity() {
else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2) else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2)
} }
return showCounter
}
val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val eventsShown = setupMenu(maxButtonCount, eventTypesSorted)
// store left over events for popup menu // store left over events for popup menu
currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size) currentPopupItems = eventTypesSorted.subList(showCounter, eventTypesSorted.size)
} }
override fun onStart() { override fun onStart() {
@@ -277,6 +272,9 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false numberPicker.wrapSelectorWheel = false
numberPicker.value = event.quantity / 10 numberPicker.value = event.quantity / 10
val numberPickerUnit = dialogView.findViewById<TextView>(R.id.dialog_number_picker_unit)
numberPickerUnit.text = NumericUtils(this).measurement_unit_liquid_base
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
@@ -423,11 +421,11 @@ class MainActivity : AppCompatActivity() {
return dateTime return dateTime
} }
fun addSleepEvent(event: LunaEvent) { fun addDurationEvent(event: LunaEvent) {
askSleepValue(event, true) { saveEvent(event) } askDurationEvent(event, true) { saveEvent(event) }
} }
fun askSleepValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askDurationEvent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null)
d.setTitle(event.getDialogTitle(this)) d.setTitle(event.getDialogTitle(this))
@@ -439,7 +437,7 @@ class MainActivity : AppCompatActivity() {
val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter) val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons) val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now) val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val durationAsleepButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_asleep) val durationClearButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_clear)
val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5) val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5)
val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5) val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5)
@@ -447,8 +445,8 @@ class MainActivity : AppCompatActivity() {
val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger) val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger)
// in seconds // in seconds
var sleepBegin = event.time var durationStart = event.getStartTime()
var sleepEnd = event.time + event.quantity var durationEnd = event.getEndTime()
fun isValidTime(timeUnix: Long): Boolean { fun isValidTime(timeUnix: Long): Boolean {
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
@@ -459,46 +457,43 @@ class MainActivity : AppCompatActivity() {
return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60) return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
} }
// prevent printing of seconds
fun adjustToMinute(unixTime: Long): Long {
return unixTime - (unixTime % 60)
}
fun updateFields() { fun updateFields() {
datePickerBegin.text = DateUtils.formatDateTime(sleepBegin) datePickerBegin.text = DateUtils.formatDateTime(durationStart)
datePickerEnd.text = DateUtils.formatDateTime(sleepEnd) datePickerEnd.text = DateUtils.formatDateTime(durationEnd)
durationTextView.setTextColor(currentDurationTextColor) durationTextView.setTextColor(currentDurationTextColor)
val duration = sleepEnd - sleepBegin val duration = durationEnd - durationStart
if (duration == 0L) { if (duration == 0L) {
// baby is sleeping // event is ongoing
durationTextView.text = "💤" durationTextView.text = "💤"
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
} else { } else {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration) durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration)
if (!isValidTimeSpan(sleepBegin, sleepEnd)) { if (!isValidTimeSpan(durationStart, durationEnd)) {
durationTextView.setTextColor(invalidDurationTextColor) durationTextView.setTextColor(invalidDurationTextColor)
} }
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
} }
datePickerBegin.setTextColor(if (isValidTime(sleepBegin)) { currentDurationTextColor } else { invalidDurationTextColor }) datePickerBegin.setTextColor(if (isValidTime(durationStart)) { currentDurationTextColor } else { invalidDurationTextColor })
datePickerEnd.setTextColor(if (isValidTime(sleepEnd)) { currentDurationTextColor } else { invalidDurationTextColor }) datePickerEnd.setTextColor(if (isValidTime(durationEnd)) { currentDurationTextColor } else { invalidDurationTextColor })
} }
val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { time: Long -> val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { pickedTime: Long ->
sleepBegin = adjustToMinute(time) durationStart = pickedTime
if (datePickerEnd.visibility == View.GONE) {
durationEnd = pickedTime
}
updateFields() updateFields()
} }
val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { time: Long -> val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { pickedTime: Long ->
sleepEnd = adjustToMinute(time) durationEnd = pickedTime
updateFields() updateFields()
} }
sleepBegin = adjustToMinute(pickedDateTimeBegin.time.time / 1000)
sleepEnd = adjustToMinute(pickedDateTimeEnd.time.time / 1000)
updateFields()
if (showTime) { if (showTime) {
dateDelimiter.visibility = View.GONE dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE datePickerEnd.visibility = View.GONE
@@ -513,31 +508,35 @@ class MainActivity : AppCompatActivity() {
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
} }
durationStart = pickedDateTimeBegin.time.time / 1000
durationEnd = pickedDateTimeEnd.time.time / 1000
updateFields()
durationMinus5Button.setOnClickListener { durationMinus5Button.setOnClickListener {
sleepEnd = (sleepEnd - 300).coerceAtLeast(sleepBegin) durationEnd = (durationEnd - 300).coerceAtLeast(durationStart)
updateFields() updateFields()
} }
durationPlus5Button.setOnClickListener { durationPlus5Button.setOnClickListener {
sleepEnd = (sleepEnd + 300).coerceAtLeast(sleepBegin) durationEnd = (durationEnd + 300).coerceAtLeast(durationStart)
updateFields() updateFields()
} }
durationAsleepButton.setOnClickListener { durationClearButton.setOnClickListener {
sleepEnd = sleepBegin durationEnd = durationStart
updateFields() updateFields()
} }
durationNowButton.setOnClickListener { durationNowButton.setOnClickListener {
val now = System.currentTimeMillis() / 1000 durationEnd = System.currentTimeMillis() / 1000
sleepEnd = adjustToMinute(now)
updateFields() updateFields()
} }
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
if (isValidTime(sleepBegin) && isValidTime(sleepEnd) && isValidTimeSpan(sleepBegin, sleepEnd)) { if (isValidTime(durationStart) && isValidTime(durationEnd) && isValidTimeSpan(durationStart, durationEnd)) {
event.time = sleepBegin event.time = durationStart
event.quantity = (sleepEnd - sleepBegin).toInt() event.quantity = (durationEnd - durationStart).toInt()
onPositive() onPositive()
} else { } else {
Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show()
@@ -883,7 +882,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues) LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues)
LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues) LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues)
LunaEvent.Type.NOTE -> askNotes(event, false, updateValues) LunaEvent.Type.NOTE -> askNotes(event, false, updateValues)
LunaEvent.Type.SLEEP -> askSleepValue(event, false, updateValues) LunaEvent.Type.SLEEP -> askDurationEvent(event, false, updateValues)
else -> { else -> {
Log.w(TAG, "Unexpected type: ${event.type}") Log.w(TAG, "Unexpected type: ${event.type}")
} }
@@ -1310,7 +1309,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.FOOD -> addNoteEvent(event) LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event) LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event) LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addSleepEvent(event) LunaEvent.Type.SLEEP -> addDurationEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore LunaEvent.Type.UNKNOWN -> {} // ignore
} }
} }

View File

@@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import com.github.mikephil.charting.animation.ChartAnimator import com.github.mikephil.charting.animation.ChartAnimator
import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry import com.github.mikephil.charting.data.BarEntry
@@ -180,14 +181,11 @@ class StatisticsActivity : AppCompatActivity() {
val values = ArrayList<BarEntry>() val values = ArrayList<BarEntry>()
val stack = ArrayList(List(state.endSpan - state.startSpan + 1) { IntArray(24 * 60 * 60 / SLEEP_PATTERN_GRANULARITY) }) val stack = ArrayList(List(state.endSpan - state.startSpan + 1) { IntArray(24 * 60 * 60 / SLEEP_PATTERN_GRANULARITY) })
Log.d(TAG, "stack.size: ${stack.size}, array.size: ${stack[0].size}, dayCounter.daysWithData.size: ${state.dayCounter.daysWithData.size}")
fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) { fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) {
val beginDays = unixToDays(begin) val beginDays = unixToDays(begin)
val endDays = unixToDays(end) val endDays = unixToDays(end)
var mid = begin var mid = begin
//Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}")
for (i in beginDays..endDays) { for (i in beginDays..endDays) {
// i is the days/weeks/months since unix epoch // i is the days/weeks/months since unix epoch
val dayBegin = daysToUnix(i) val dayBegin = daysToUnix(i)
@@ -200,7 +198,6 @@ class StatisticsActivity : AppCompatActivity() {
assert(sleepBegin <= sleepEnd) assert(sleepBegin <= sleepEnd)
val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY
val iEnd = iBegin + (sleepEnd - sleepBegin) / SLEEP_PATTERN_GRANULARITY val iEnd = iBegin + (sleepEnd - sleepBegin) / SLEEP_PATTERN_GRANULARITY
//Log.d(TAG, "index: $index, iBegin: $iBegin, iEnd: $iEnd, dayBegin: ${Date(dayBegin * 1000)}, dayEnd: ${Date(dayEnd * 1000)}, sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}")
for (j in iBegin..<iEnd) { for (j in iBegin..<iEnd) {
stack[index][j.toInt()] += 1 stack[index][j.toInt()] += 1
} }
@@ -218,17 +215,14 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix) val endIndex = unixToSpan(endUnix)
var mid = startUnix var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) { for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch // i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i) val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1) val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin) val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd) val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan val index = i - state.startSpan
val duration = sleepEnd - sleepBegin val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd) state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd) stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -255,7 +249,6 @@ class StatisticsActivity : AppCompatActivity() {
for ((index, dayArray) in stack.withIndex()) { for ((index, dayArray) in stack.withIndex()) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "index: $index: daysWithData: $daysWithData, dayArray: ${dayArray.joinToString { it.toString() }}")
val vals = ArrayList<Float>() val vals = ArrayList<Float>()
var prevIndex = -1 // time slot index var prevIndex = -1 // time slot index
@@ -277,14 +270,10 @@ class StatisticsActivity : AppCompatActivity() {
allColors.add(mapColor(prevValue, daysWithData)) allColors.add(mapColor(prevValue, daysWithData))
} }
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}") assert(values.size == index)
values.add(BarEntry(values.size.toFloat(), vals.toFloatArray()))
//Log.d(TAG, "index: ${index.toFloat()}")
values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
} }
//Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}")
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) { override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) { if (e == null || h == null) {
@@ -308,7 +297,6 @@ class StatisticsActivity : AppCompatActivity() {
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index) val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index)
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}")
val startSeconds = val startSeconds =
SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex) SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex)
.fold(0) { acc, y -> acc + y.toInt() } .fold(0) { acc, y -> acc + y.toInt() }
@@ -350,28 +338,21 @@ class StatisticsActivity : AppCompatActivity() {
}) })
val set1 = BarDataSet(values, "") val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.colors = allColors set1.colors = allColors
set1.setDrawValues(false) // usually too many values set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true set1.isHighlightEnabled = true
set1.setDrawIcons(false) set1.setDrawIcons(false)
//Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}") val data = BarData(set1)
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data)
// does not work quite right yet
//Log.d(TAG, "xChartMax: ${barChart.xChartMax}")
//barChart.centerViewTo(barChart.xChartMax, 0F, YAxis.AxisDependency.LEFT)
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24) val valueCount = min(values.size, 24)
barChart.setData(data)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat()) barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false) barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate() barChart.invalidate()
} }
@@ -402,7 +383,8 @@ class StatisticsActivity : AppCompatActivity() {
// update value // update value
val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat()) val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat())
values[index] = BarEntry(x, newYVals) assert(index == x.toInt())
values[index] = BarEntry(index.toFloat(), newYVals)
} }
for (range in ranges) { for (range in ranges) {
@@ -414,17 +396,14 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix) val endIndex = unixToSpan(endUnix)
var mid = startUnix var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) { for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch // i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i) val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1) val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin) val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd) val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan val index = i - state.startSpan
val duration = sleepEnd - sleepBegin val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd) state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd) stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -433,12 +412,11 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
val set1 = BarDataSet(values, "")
val data = BarData(set1)
// awake phase color is transparent // awake phase color is transparent
val set1 = BarDataSet(values, "")
set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt()) set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values set1.setDrawValues(false) // usually too many values
set1.setDrawIcons(false)
set1.isHighlightEnabled = true set1.isHighlightEnabled = true
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
@@ -478,19 +456,17 @@ class StatisticsActivity : AppCompatActivity() {
override fun onNothingSelected() {} override fun onNothingSelected() {}
}) })
//Log.d(TAG, "showSleepPatternBarGraphDaily: values.size: ${values.size}, barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
set1.setDrawIcons(false)
val data = BarData(set1)
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data)
//Log.d(TAG, "showSleepPatternBarGraphDaily: new barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24) val valueCount = min(values.size, 24)
barChart.setData(data)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat()) barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false) barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate() barChart.invalidate()
} }
@@ -511,7 +487,6 @@ class StatisticsActivity : AppCompatActivity() {
fun showSleepBarGraph(state: GraphState) { fun showSleepBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events) val ranges = toSleepRanges(state.events)
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) }) val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
//Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
for (range in ranges) { for (range in ranges) {
// a sleep event can span to another day // a sleep event can span to another day
@@ -522,18 +497,14 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix) val endIndex = unixToSpan(endUnix)
var mid = startUnix var mid = startUnix
//Log.d(TAG, "beginIndex: $begIndex, endIndex: $endIndex, startUnix: ${Date(startUnix * 1000)} ($startUnix), endUnix: ${Date(endUnix * 1000)} ($endUnix)")
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) { for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch // i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i) val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1) val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "i: $i, mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin) val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd) val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan val index = i - state.startSpan
val duration = sleepEnd - sleepBegin val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd) state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
@@ -592,6 +563,7 @@ class StatisticsActivity : AppCompatActivity() {
barChart.setVisibleXRangeMaximum(valueCount.toFloat()) barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false) barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate() barChart.invalidate()
} }
@@ -615,7 +587,6 @@ class StatisticsActivity : AppCompatActivity() {
for (index in values.indices) { for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "values[$index].y: ${values[index].y}, daysWithData: $daysWithData")
if (daysWithData == 0) { if (daysWithData == 0) {
assert(values[index].y == 0F) assert(values[index].y == 0F)
} else { } else {
@@ -632,7 +603,6 @@ class StatisticsActivity : AppCompatActivity() {
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" } val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) { return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> { GraphType.BOTTLE_EVENTS -> {
prefix + value.toInt().toString() prefix + value.toInt().toString()
@@ -654,6 +624,7 @@ class StatisticsActivity : AppCompatActivity() {
barChart.setVisibleXRangeMaximum(valueCount.toFloat()) barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false) barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate() barChart.invalidate()
} }
@@ -665,10 +636,8 @@ class StatisticsActivity : AppCompatActivity() {
fun countDaysWithData(beginUnix: Long, endUnix: Long): Int { fun countDaysWithData(beginUnix: Long, endUnix: Long): Int {
val beginDays = unixToDays(beginUnix) val beginDays = unixToDays(beginUnix)
val endDays = unixToDays(endUnix) val endDays = unixToDays(endUnix)
//Log.d(TAG, "countDaysWithData: beginDays: $beginDays, endDays: $endDays, ${Date(beginUnix * 1000)} - ${Date(endUnix * 1000)}")
var count = 0 var count = 0
for (i in (beginDays - startDays)..<(endDays - startDays)) { for (i in (beginDays - startDays)..<(endDays - startDays)) {
//Log.d(TAG, "countDaysWithData: i: $i, size: ${daysWithData.size}")
count += if (daysWithData[i]) { 1 } else { 0 } count += if (daysWithData[i]) { 1 } else { 0 }
} }
return count return count
@@ -688,14 +657,12 @@ class StatisticsActivity : AppCompatActivity() {
data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int) data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int)
fun showGraph() { fun showGraph() {
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
barChart.fitScreen() barChart.fitScreen()
barChart.data?.clearValues() barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged() barChart.notifyDataSetChanged()
barChart.clear() barChart.clear()
barChart.invalidate() barChart.invalidate()
//Log.d(TAG, "resetBarChart; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}")
val type = when (graphTypeSelection) { val type = when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS, GraphType.BOTTLE_EVENTS,
@@ -705,7 +672,6 @@ class StatisticsActivity : AppCompatActivity() {
GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP
} }
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time } val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) { if (events.isEmpty()) {
@@ -730,8 +696,6 @@ class StatisticsActivity : AppCompatActivity() {
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span
val dayCounter = DayCounter(startDays, endDays) val dayCounter = DayCounter(startDays, endDays)
//Log.d(TAG, "startUnix: ${Date(1000 * startUnix)}, endUnix: ${Date(1000 * endUnix)}, startSpan: ${Date(1000 * spanToUnix(startSpan))}, endSpan: ${Date(1000 * spanToUnix(endSpan))}, startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
// print dates // print dates
barChart.xAxis.valueFormatter = object: ValueFormatter() { barChart.xAxis.valueFormatter = object: ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
@@ -745,8 +709,6 @@ class StatisticsActivity : AppCompatActivity() {
val week = dateTime.get(Calendar.WEEK_OF_YEAR) val week = dateTime.get(Calendar.WEEK_OF_YEAR)
val day = dateTime.get(Calendar.DAY_OF_MONTH) val day = dateTime.get(Calendar.DAY_OF_MONTH)
//Log.d(TAG, "index: $index, unixSeconds: ${Date(1000 * unixSeconds)}, day: $day, week: $week, month: $month, year: $year")
// Adjust years if the first week of a year starts in the previous year. // Adjust years if the first week of a year starts in the previous year.
val years = if (month == 12 && week == 1) { val years = if (month == 12 && week == 1) {
year + 1 year + 1
@@ -798,8 +760,6 @@ class StatisticsActivity : AppCompatActivity() {
val spinnerAdapter = val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item) ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
//Log.d(TAG, "spinner ${arrayValues.indexOf(currentValue)} (${arrayValues.joinToString { it }})")
spinner.adapter = spinnerAdapter spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue)) spinner.setSelection(arrayValues.indexOf(currentValue))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {

View File

@@ -174,7 +174,7 @@ class LunaEvent: Comparable<LunaEvent> {
Type.DIAPERCHANGE_PEE, Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_dialog_description Type.PUKE -> R.string.log_amount_dialog_description
Type.WEIGHT -> R.string.log_weight_dialog_description Type.WEIGHT -> R.string.log_weight_dialog_description
Type.SLEEP -> R.string.log_sleep_dialog_description Type.SLEEP -> R.string.log_duration_dialog_description
else -> R.string.log_unknown_dialog_description else -> R.string.log_unknown_dialog_description
} }
) )

View File

@@ -9,10 +9,15 @@ import java.util.Date
class DateUtils { class DateUtils {
companion object { companion object {
/** /**
* Format time duration in seconds as e.g. "2 hours, 1 min". * Format time duration in seconds as e.g. "2 hours, 1 min", rounded to minutes.
* Used for the duration to the next/previous event in the event details dialog. * Used for the duration to the next/previous event in the event details dialog.
*/ */
fun formatTimeDuration(context: Context, secondsDiff: Long): String { fun formatTimeDuration(context: Context, secondsDiff: Long): String {
val adjusted = (secondsDiff + 30) - (secondsDiff + 30) % 60
return formatTimeDurationExact(context, adjusted)
}
fun formatTimeDurationExact(context: Context, secondsDiff: Long): String {
var seconds = secondsDiff var seconds = secondsDiff
val years = (seconds / (365 * 24 * 60 * 60F)).toLong() val years = (seconds / (365 * 24 * 60 * 60F)).toLong()

View File

@@ -18,6 +18,7 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/dialog_number_picker_unit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"

View File

@@ -39,11 +39,11 @@
android:text="@string/dialog_duration_button_now"/> android:text="@string/dialog_duration_button_now"/>
<Button <Button
android:id="@+id/dialog_date_duration_asleep" android:id="@+id/dialog_date_duration_clear"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/dialog_duration_button_asleep"/> android:text="@string/dialog_duration_button_clear"/>
<Button <Button
android:id="@+id/dialog_date_duration_plus5" android:id="@+id/dialog_date_duration_plus5"

View File

@@ -130,7 +130,7 @@
<string name="log_temperature_dialog_description">Select the temperature:</string> <string name="log_temperature_dialog_description">Select the temperature:</string>
<string name="log_unknown_dialog_description"></string> <string name="log_unknown_dialog_description"></string>
<string name="log_weight_dialog_description">Insert the weight:</string> <string name="log_weight_dialog_description">Insert the weight:</string>
<string name="log_sleep_dialog_description">Set sleep duration:</string> <string name="log_duration_dialog_description">Set duration:</string>
<string name="measurement_unit_liquid_base_metric" translatable="false">ml</string> <string name="measurement_unit_liquid_base_metric" translatable="false">ml</string>
<string name="measurement_unit_weight_base_metric" translatable="false">g</string> <string name="measurement_unit_weight_base_metric" translatable="false">g</string>
@@ -160,9 +160,9 @@
<string name="dialog_event_detail_notes">Notes</string> <string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string> <string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_duration_button_asleep">asleep</string> <string name="dialog_duration_button_clear">Clear</string>
<string name="dialog_duration_button_minus_5">-5 min</string> <string name="dialog_duration_button_minus_5">-5 min</string>
<string name="dialog_duration_button_now">now</string> <string name="dialog_duration_button_now">Now</string>
<string name="dialog_duration_button_plus_5">+5 min</string> <string name="dialog_duration_button_plus_5">+5 min</string>
<string name="dialog_add_logbook_title">Add logbook</string> <string name="dialog_add_logbook_title">Add logbook</string>