16 Commits

Author SHA1 Message Date
c636e48c7e 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-01-12 22:46:05 +01:00
f39882edc4 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-01-12 22:46:05 +01:00
0683f396ae StatisticsActivity: rework all statistics
Improve the overall code.
2026-01-12 22:46:02 +01:00
2a446ea7d3 NumericUtils: remove possible trailing whitespace 2026-01-11 21:55:07 +01:00
b753703ff3 MainActivity: do not switch logbook on reload 2026-01-11 21:55:07 +01:00
f5bd345e23 LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2026-01-11 21:55:07 +01:00
dc0cd6353c MainAcitivty: add dynamic header setting
The setting allows to build the menu and
popup list to be populated by the frequency
of events that has been created.
This also makes the 'no breastfeeding' setting irrelevant.
2026-01-11 21:55:07 +01:00
7a0343f464 LunaEvent: use enum class for event types
This helps to have compile errors when some
case it not handled while adding a new type.
The enum class can also be interated over
to create a complete drop down list.
2026-01-11 21:55:03 +01:00
389514ec4f MainActivity: increase bottle volume to 340ml
This is the maximum amount found in sold bottles.
2026-01-11 21:53:46 +01:00
7608fc756b gradle: use uniform implementation directive for sardine-android 2026-01-11 21:53:46 +01:00
ced76d449e gradle: avoid inclusion of apk signing blobs
See https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
2026-01-11 21:53:46 +01:00
64e4fbbba2 gradle: set compileSDK/targetSdk to 36 2026-01-11 21:53:46 +01:00
98cf9587e8 StatisticsActivity: add statistics for bottle and sleep events 2026-01-11 21:53:46 +01:00
3faaf6d6f0 MainActivity: show save button if any values has changed 2026-01-11 21:53:46 +01:00
86721fbbae MainActivity: use unique templates for notes 2026-01-11 21:53:46 +01:00
155d53a6f0 LunaEvent: add sleep event 2026-01-11 21:53:41 +01:00
14 changed files with 399 additions and 482 deletions

View File

@@ -31,7 +31,7 @@
android:label="@string/settings_title"
android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".LongTermStatisticsActivity"
android:name=".StatisticsActivity"
android:label="@string/statistics_title"
android:theme="@style/Theme.LunaTracker"/>
</application>

View File

@@ -148,69 +148,72 @@ class MainActivity : AppCompatActivity() {
val eventTypeStats = mutableMapOf<LunaEvent.Type, Int>()
if (dynamicMenu) {
// populate frequency map from all events of the last two weeks
val lastWeekTime = (System.currentTimeMillis() / 1000) - (14 * 24 * 60 * 60)
allEvents.forEach {
if (it.time > lastWeekTime) {
eventTypeStats[it.type] = 1 + (eventTypeStats[it.type] ?: 0)
}
val sampleSize = 100
// populate frequency map from first 100 events
allEvents.take(sampleSize.coerceAtMost(allEvents.size)).forEach {
eventTypeStats[it.type] = 1 + (eventTypeStats[it.type] ?: 0)
}
}
// sort all event types by frequency and ordinal
// sort all event types by frequency or ordinal
val eventTypesSorted = LunaEvent.Type.entries.toList().sortedWith(
compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal })
).filter { it != LunaEvent.Type.UNKNOWN }
fun setupMenu(maxButtonCount: Int, sortedEventTypes: List<LunaEvent.Type>): Int {
val row1 = findViewById<View>(R.id.linear_layout_row1)
val row1Button1 = findViewById<TextView>(R.id.button1_row1)
val row1Button2 = findViewById<TextView>(R.id.button2_row1)
val row2 = findViewById<View>(R.id.linear_layout_row2)
val row2Button1 = findViewById<TextView>(R.id.button1_row2)
val row2Button2 = findViewById<TextView>(R.id.button2_row2)
val row2Button3 = findViewById<TextView>(R.id.button3_row2)
val row3 = findViewById<View>(R.id.linear_layout_row3)
val row3Button1 = findViewById<TextView>(R.id.button1_row3)
val row3Button2 = findViewById<TextView>(R.id.button2_row3)
// hide all rows/buttons (except row 3)
for (view in listOf(row1, row1Button1, row1Button2,
row2, row2Button1, row2Button2, row2Button3,
row3, row3Button1, row3Button2)) {
view.visibility = View.GONE
}
row3.visibility = View.VISIBLE
var showCounter = 0
fun show(vararg tvs: TextView) {
for (tv in tvs) {
val type = sortedEventTypes[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE
// show parent row
(tv.parent as View).visibility = View.VISIBLE
showCounter += 1
}
}
when (maxButtonCount) {
0 -> { } // ignore - show empty row3
1 -> show(row3Button1)
2 -> show(row3Button1, row3Button2)
3 -> show(row1Button1, row3Button1)
4, 5, 6 -> show(row1Button1, row1Button2, 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 row1 = findViewById<View>(R.id.linear_layout_row1)
val row1Button1 = findViewById<TextView>(R.id.button1_row1)
val row1Button2 = findViewById<TextView>(R.id.button2_row1)
val row2 = findViewById<View>(R.id.linear_layout_row2)
val row2Button1 = findViewById<TextView>(R.id.button1_row2)
val row2Button2 = findViewById<TextView>(R.id.button2_row2)
val row2Button3 = findViewById<TextView>(R.id.button3_row2)
val row3 = findViewById<View>(R.id.linear_layout_row3)
val row3Button1 = findViewById<TextView>(R.id.button1_row3)
val row3Button2 = findViewById<TextView>(R.id.button2_row3)
// hide all rows/buttons (except row 3)
for (view in listOf(row1, row1Button1, row1Button2,
row2, row2Button1, row2Button2, row2Button3,
row3, row3Button1, row3Button2)) {
view.visibility = View.GONE
}
row3.visibility = View.VISIBLE
var showCounter = 0
fun show(vararg tvs: TextView) {
for (tv in tvs) {
val type = eventTypesSorted[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE
// show parent row
(tv.parent as View).visibility = View.VISIBLE
showCounter += 1
}
}
when (maxButtonCount) {
0 -> { } // ignore - show empty row3
1 -> show(row3Button1)
2 -> show(row3Button1, row3Button2)
3 -> show(row1Button1, row3Button1)
4, 5, 6 -> show(row1Button1, row1Button2, row3Button1, row3Button2)
else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2)
}
val eventsShown = setupMenu(maxButtonCount, eventTypesSorted)
// store left over events for popup menu
currentPopupItems = eventTypesSorted.subList(showCounter, eventTypesSorted.size)
currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size)
}
override fun onStart() {
@@ -272,11 +275,8 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false
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 pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -313,11 +313,8 @@ class MainActivity : AppCompatActivity() {
val weightET = dialogView.findViewById<EditText>(R.id.dialog_number_edittext)
weightET.setText(event.quantity.toString())
val unitTV = dialogView.findViewById<TextView>(R.id.dialog_number_unit)
unitTV.text = NumericUtils(this).measurement_unit_weight_base
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -344,53 +341,6 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun addHeightEvent(event: LunaEvent) {
setToPreviousQuantity(event)
askHeightValue(event, true) { saveEvent(event) }
}
fun askHeightValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
// Show number picker dialog
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_height, null)
d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this))
d.setView(dialogView)
val heightET = dialogView.findViewById<EditText>(R.id.dialog_number_edittext)
heightET.setText(event.quantity.toString())
val unitTV = dialogView.findViewById<TextView>(R.id.dialog_number_unit)
unitTV.text = NumericUtils(this).measurement_unit_height_base
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val height = heightET.text.toString().toIntOrNull()
if (height != null) {
event.time = pickedTime.time.time / 1000
event.quantity = height
onPositive()
} else {
Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show()
}
dialogInterface.dismiss()
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i ->
dialogInterface.dismiss()
}
val alertDialog = d.create()
alertDialog.show()
}
fun addTemperatureEvent(event: LunaEvent) {
setToPreviousQuantity(event)
askTemperatureValue(event, true) { saveEvent(event) }
@@ -415,7 +365,7 @@ class MainActivity : AppCompatActivity() {
}
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -439,8 +389,7 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
// Pick a date/time.
fun dateTimePicker(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
fun datePickerHelper(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
dateTextView.text = DateUtils.formatDateTime(time)
val dateTime = Calendar.getInstance()
@@ -471,110 +420,84 @@ class MainActivity : AppCompatActivity() {
return dateTime
}
fun addDurationEvent(event: LunaEvent) {
askDurationEvent(event, true) { saveEvent(event) }
fun addSleepEvent(event: LunaEvent) {
askSleepValue(event, true) { saveEvent(event) }
}
fun askDurationEvent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
fun askSleepValue(event: LunaEvent, hideDurationButtons: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null)
d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this))
d.setView(dialogView)
val durationTV = dialogView.findViewById<TextView>(R.id.dialog_date_duration)
val datePickerBeginTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker_begin)
val datePickerEndTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker_end)
val dateDelimiterTV = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter)
val durationTextView = dialogView.findViewById<TextView>(R.id.dialog_date_duration)
val datePicker = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val durationClearButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_clear)
val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5)
val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5)
val currentDurationTextColor = durationTV.currentTextColor
val currentDurationTextColor = durationTextView.currentTextColor
val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger)
// in seconds
var durationStart = event.getStartTime()
var durationEnd = event.getEndTime()
var duration = event.quantity
fun isValidTime(timeUnix: Long): Boolean {
fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean {
val now = System.currentTimeMillis() / 1000
return timeUnix in 1..now
return (timeSeconds + durationSeconds) <= now && durationSeconds < (24 * 60 * 60)
}
fun isValidTimeSpan(timeBeginUnix: Long, timeEndUnix: Long): Boolean {
return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
}
val onDateChange = { time: Long ->
durationTextView.setTextColor(currentDurationTextColor)
fun updateFields() {
datePickerBeginTV.text = DateUtils.formatDateTime(durationStart)
datePickerEndTV.text = DateUtils.formatDateTime(durationEnd)
dateTimePicker(durationStart, datePickerBeginTV) { pickedTime: Long ->
durationStart = pickedTime
if (datePickerEndTV.visibility == View.GONE) {
durationEnd = pickedTime
}
updateFields()
}
dateTimePicker(durationEnd, datePickerEndTV) { pickedTime: Long ->
durationEnd = pickedTime
updateFields()
}
durationTV.setTextColor(currentDurationTextColor)
val duration = durationEnd - durationStart
if (duration == 0L) {
// event is ongoing
durationTV.text = "💤"
dateDelimiterTV.visibility = View.GONE
datePickerEndTV.visibility = View.GONE
if (duration == 0) {
// baby is sleeping
durationTextView.text = "💤"
} else {
durationTV.text = DateUtils.formatTimeDuration(applicationContext, duration)
if (!isValidTimeSpan(durationStart, durationEnd)) {
durationTV.setTextColor(invalidDurationTextColor)
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
if (!isValidTime(time, duration)) {
durationTextView.setTextColor(invalidDurationTextColor)
}
dateDelimiterTV.visibility = View.VISIBLE
datePickerEndTV.visibility = View.VISIBLE
}
}
val pickedDateTime = datePickerHelper(event.time, datePicker, onDateChange)
onDateChange(pickedDateTime.time.time / 1000)
if (hideDurationButtons) {
durationButtons.visibility = View.GONE
} else {
durationButtons.visibility = View.VISIBLE
fun adjust(minutes: Int) {
duration += minutes * 60
if (duration < 0) {
duration = 0
}
onDateChange(pickedDateTime.time.time / 1000)
}
val colorBegin = if (isValidTime(durationStart)) { currentDurationTextColor } else { invalidDurationTextColor }
datePickerBeginTV.setTextColor(colorBegin)
durationMinus5Button.setOnClickListener { adjust(-5) }
durationPlus5Button.setOnClickListener { adjust(5) }
val colorEnd = if (isValidTime(durationEnd)) { currentDurationTextColor } else { invalidDurationTextColor }
datePickerEndTV.setTextColor(colorEnd)
}
if (showTime) {
dateDelimiterTV.visibility = View.GONE
datePickerEndTV.visibility = View.GONE
durationTV.visibility = View.GONE
durationButtons.visibility = View.GONE
//d.setMessage("")
} else {
dateDelimiterTV.visibility = View.VISIBLE
datePickerEndTV.visibility = View.VISIBLE
durationTV.visibility = View.VISIBLE
durationButtons.visibility = View.VISIBLE
d.setMessage(event.getDialogMessage(this))
}
updateFields()
durationClearButton.setOnClickListener {
durationEnd = durationStart
updateFields()
}
durationNowButton.setOnClickListener {
durationEnd = System.currentTimeMillis() / 1000
updateFields()
durationNowButton.setOnClickListener {
val now = System.currentTimeMillis() / 1000
val start = pickedDateTime.time.time / 1000
if (now > start) {
duration = (now - start).toInt()
duration -= duration % 60 // prevent printing of seconds
onDateChange(pickedDateTime.time.time / 1000)
}
}
}
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
if (isValidTime(durationStart) && isValidTime(durationEnd) && isValidTimeSpan(durationStart, durationEnd)) {
event.time = durationStart
event.quantity = (durationEnd - durationStart).toInt()
val time = pickedDateTime.time.time / 1000
if (isValidTime(time, duration)) {
event.time = time
event.quantity = duration
onPositive()
} else {
Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show()
@@ -591,7 +514,6 @@ class MainActivity : AppCompatActivity() {
}
fun addAmountEvent(event: LunaEvent) {
setToPreviousQuantity(event)
askAmountValue(event, true) { saveEvent(event) }
}
@@ -608,11 +530,11 @@ class MainActivity : AppCompatActivity() {
R.array.AmountLabels,
android.R.layout.simple_spinner_dropdown_item
)
// set pre-selected item and ensure the quantity to index is in bounds
spinner.setSelection(event.quantity.coerceIn(0, spinner.count - 1))
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -645,7 +567,7 @@ class MainActivity : AppCompatActivity() {
d.setView(dialogView)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedDateTime = dateTimePicker(event.time, dateTV)
val pickedDateTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -680,7 +602,7 @@ class MainActivity : AppCompatActivity() {
val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -695,6 +617,9 @@ class MainActivity : AppCompatActivity() {
var nextEvent = getNextSameEvent(current, templates)
notesET.setText(current.notes)
if (useQuantity) {
qtyET.setText(current.quantity.toString())
}
if (nextEvent == null && current != event) {
nextEvent = event
@@ -703,6 +628,9 @@ class MainActivity : AppCompatActivity() {
if (nextEvent != null) {
nextTextView.setOnClickListener {
notesET.setText(nextEvent.notes)
if (useQuantity) {
qtyET.setText(nextEvent.quantity.toString())
}
updateContent(nextEvent)
}
nextTextView.alpha = 1.0f
@@ -714,6 +642,9 @@ class MainActivity : AppCompatActivity() {
if (prevEvent != null) {
prevTextView.setOnClickListener {
notesET.setText(prevEvent.notes)
if (useQuantity) {
qtyET.setText(prevEvent.quantity.toString())
}
updateContent(prevEvent)
}
prevTextView.alpha = 1.0f
@@ -883,7 +814,7 @@ class MainActivity : AppCompatActivity() {
quantityTextView.text = NumericUtils(this).formatEventQuantity(event)
notesTextView.text = event.notes
if (event.type == LunaEvent.Type.SLEEP && event.quantity > 0) {
dateEndTextView.text = DateUtils.formatDateTime(event.getEndTime())
dateEndTextView.text = DateUtils.formatDateTime(event.time + event.quantity)
dateEndTextView.visibility = View.VISIBLE
} else {
dateEndTextView.visibility = View.GONE
@@ -897,7 +828,7 @@ class MainActivity : AppCompatActivity() {
}
updateValues()
dateTimePicker(event.time, dateTextView, { newTime: Long ->
datePickerHelper(event.time, dateTextView, { newTime: Long ->
event.time = newTime
updateValues()
})
@@ -906,15 +837,12 @@ class MainActivity : AppCompatActivity() {
when (event.type) {
LunaEvent.Type.BABY_BOTTLE -> askBabyBottleContent(event, false, updateValues)
LunaEvent.Type.WEIGHT -> askWeightValue(event, false, updateValues)
LunaEvent.Type.HEIGHT -> askHeightValue(event, false, updateValues)
LunaEvent.Type.DIAPERCHANGE_POO,
LunaEvent.Type.DIAPERCHANGE_PEE,
LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues)
LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues)
LunaEvent.Type.FOOD,
LunaEvent.Type.MEDICINE,
LunaEvent.Type.NOTE -> askNotes(event, false, updateValues)
LunaEvent.Type.SLEEP -> askDurationEvent(event, false, updateValues)
LunaEvent.Type.SLEEP -> askSleepValue(event, false, updateValues)
else -> {
Log.w(TAG, "Unexpected type: ${event.type}")
}
@@ -944,7 +872,7 @@ class MainActivity : AppCompatActivity() {
val previousEvent = getPreviousSameEvent(event, allEvents)
if (previousEvent != null) {
val emoji = previousEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, event.getStartTime() - previousEvent.getEndTime())
val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time)
previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener {
alertDialog.cancel()
@@ -959,7 +887,7 @@ class MainActivity : AppCompatActivity() {
val nextEvent = getNextSameEvent(event, allEvents)
if (nextEvent != null) {
val emoji = nextEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.getStartTime() - event.getEndTime())
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time)
nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener {
alertDialog.cancel()
@@ -1328,7 +1256,6 @@ class MainActivity : AppCompatActivity() {
when (type) {
LunaEvent.Type.BABY_BOTTLE -> addBabyBottleEvent(event)
LunaEvent.Type.WEIGHT -> addWeightEvent(event)
LunaEvent.Type.HEIGHT -> addHeightEvent(event)
LunaEvent.Type.BREASTFEEDING_LEFT_NIPPLE -> addPlainEvent(event)
LunaEvent.Type.BREASTFEEDING_BOTH_NIPPLE -> addPlainEvent(event)
LunaEvent.Type.BREASTFEEDING_RIGHT_NIPPLE -> addPlainEvent(event)
@@ -1342,7 +1269,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addDurationEvent(event)
LunaEvent.Type.SLEEP -> addSleepEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore
}
}
@@ -1359,7 +1286,7 @@ class MainActivity : AppCompatActivity() {
// Add statistics (hard coded)
contentView.findViewById<View>(R.id.button_statistics).setOnClickListener {
if (logbook != null && !pauseLogbookUpdate) {
val i = Intent(applicationContext, LongTermStatisticsActivity::class.java)
val i = Intent(applicationContext, StatisticsActivity::class.java)
i.putExtra("LOOGBOOK_NAME", logbook!!.name)
startActivity(i)
} else {

View File

@@ -1,6 +1,5 @@
package it.danieleverducci.lunatracker
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.Log
@@ -12,19 +11,15 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.toColorInt
import com.github.mikephil.charting.animation.ChartAnimator
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.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import com.github.mikephil.charting.renderer.HorizontalBarChartRenderer
import com.github.mikephil.charting.utils.ViewPortHandler
import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils
import utils.NumericUtils
@@ -32,10 +27,11 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class LongTermStatisticsActivity : AppCompatActivity() {
class StatisticsActivity : AppCompatActivity() {
var lastToastShown = 0L
lateinit var barChart: BarChart
@@ -51,7 +47,8 @@ class LongTermStatisticsActivity : AppCompatActivity() {
BOTTLE_SUM,
SLEEP_SUM,
SLEEP_EVENTS,
SLEEP_PATTERN
SLEEP_PATTERN,
MEDICINE_EVENTS
}
enum class TimeRange {
@@ -85,12 +82,10 @@ class LongTermStatisticsActivity : AppCompatActivity() {
barChart.axisRight.setDrawGridLines(false)
barChart.axisRight.setDrawLabels(false)
//barChart.xAxis.setDrawGridLines(true)
barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setDrawAxisLine(false)
barChart.isScaleXEnabled = false
barChart.isScaleYEnabled = true
graphTypeSpinner = findViewById(R.id.graph_type_selection)
timeRangeSpinner = findViewById(R.id.time_range_selection)
@@ -141,6 +136,84 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
}
fun showMedicineBarGraph(state: GraphState) {
val values = HashMap<String, ArrayList<BarEntry>>()
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
val key = event.notes.trim().lowercase()
val array = values.getOrPut(key) {
ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
}
array[index].y += 1F
}
/*
Log.d(TAG, "values.size: ${values.size}")
for ((key, value) in values) {
Log.d(TAG, "key: $key, value.size: ${value.size} ,value: ${value.joinToString { it.y.toLong().toString() }}")
}
*/
// make sure legend names are not too long
fun shorten(notes: String): String {
return if (notes.length > 16) {
notes.take(13) + "..."
} else {
notes
}
}
fun chooseColor(notes: String): Int {
return (abs(notes.hashCode()) * 16777215) or (0xFF shl 24)
}
val sets = arrayListOf<IBarDataSet>()
for ((key, value) in values.entries) {
if (key.startsWith("v")) {
val description = shorten(key)
val barDataSet = BarDataSet(value, description)
barDataSet.color = chooseColor(key)
sets.add(barDataSet)
}
}
val data = BarData(sets)
//data.groupBars(0F, 0.2F, 0.1F);
data.setValueTextSize(12f)
data.barWidth = 1F
//data.groupBars(0F, 1F, 1F)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return if (value == 0F) {
""
} else {
value.toInt().toString()
}
}
})
barChart.setScaleEnabled(true)
barChart.legend.isEnabled = true
//barChart.xAxis.setLabelCount(min(values.size, 24), false);
//val maxCount = min(maxIndex, 30) // values.size
//Log.d(TAG, "maxCount: $maxCount")
barChart.setVisibleXRangeMaximum(20F) //maxCount.toFloat()) // show max 24 entries
barChart.xAxis.setLabelCount(30, true)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
barChart.setData(data)
barChart.invalidate()
}
data class SleepRange(val start: Long, var end: Long)
fun toSleepRanges(events: List<LunaEvent>): ArrayList<SleepRange> {
@@ -181,11 +254,15 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val values = ArrayList<BarEntry>()
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) {
val beginDays = unixToDays(begin)
val endDays = unixToDays(end)
var mid = begin
//Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}")
for (i in beginDays..endDays) {
// i is the days/weeks/months since unix epoch
val dayBegin = daysToUnix(i)
@@ -198,7 +275,8 @@ class LongTermStatisticsActivity : AppCompatActivity() {
assert(sleepBegin <= sleepEnd)
val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY
val iEnd = iBegin + (sleepEnd - sleepBegin) / SLEEP_PATTERN_GRANULARITY
for (j in iBegin..<iEnd) {
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) {
stack[index][j.toInt()] += 1
}
}
@@ -215,14 +293,17 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix)
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) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
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 sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
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)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -232,11 +313,13 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
fun mapColor(occurrences: Int, maxOccurrences: Int): Int {
// occurrences: number of reported sleeps in a specific time slot
//Log.d(TAG, "$occurrences <= $maxOccurrences")
// occurrences: number of reported sleeps in a specific time slice
// maxOccurrences: maximum number of days with data that can contribute to maxOccurrences
assert(maxOccurrences > 0)
assert(occurrences <= maxOccurrences)
// map to color
val q = occurrences.toFloat() / maxOccurrences.toFloat()
val i = q * (SLEEP_PATTERN_COLORS.size - 1).toFloat()
@@ -249,10 +332,11 @@ class LongTermStatisticsActivity : AppCompatActivity() {
for ((index, dayArray) in stack.withIndex()) {
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>()
var prevIndex = -1 // time slot index
var prevValue = -1 // number of entries we have found for time slot
var prevIndex = -1 // time slice index
var prevValue = -1 // number of entries we have found for time slice
for ((i, v) in dayArray.withIndex()) {
if (i == 0) {
prevIndex = i
@@ -270,10 +354,13 @@ class LongTermStatisticsActivity : AppCompatActivity() {
allColors.add(mapColor(prevValue, daysWithData))
}
assert(values.size == index)
values.add(BarEntry(values.size.toFloat(), vals.toFloatArray()))
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}")
values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
}
//Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}")
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) {
@@ -297,6 +384,7 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index)
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}")
val startSeconds =
SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex)
.fold(0) { acc, y -> acc + y.toInt() }
@@ -309,7 +397,10 @@ class LongTermStatisticsActivity : AppCompatActivity() {
format.format((dayStartUnix + startSeconds) * 1000).toString()
val endTimeString =
format.format((dayStartUnix + endSeconds) * 1000).toString()
val durationString = DateUtils.formatTimeDuration(applicationContext, durationSeconds.toLong())
val durationString = NumericUtils(applicationContext).formatEventQuantity(
LunaEvent.Type.SLEEP,
durationSeconds
)
val daysWithData =
stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY]
@@ -338,26 +429,24 @@ class LongTermStatisticsActivity : AppCompatActivity() {
})
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.colors = allColors
set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true
set1.setDrawIcons(false)
val data = BarData(set1)
data.setValueTextSize(12f)
val valueCount = min(values.size, 24)
barChart.setData(data)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
// Sleep pattern bars that do not use time slots.
// This is useful/nicer for bars that only represent data of a singular days.
// This is useful/nicer for bars that only represent data of a singlur days.
fun showSleepPatternBarGraphDaily(state: GraphState) {
val ranges = toSleepRanges(state.events)
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), FloatArray(0)) })
@@ -383,8 +472,7 @@ class LongTermStatisticsActivity : AppCompatActivity() {
// update value
val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat())
assert(index == x.toInt())
values[index] = BarEntry(index.toFloat(), newYVals)
values[index] = BarEntry(x, newYVals)
}
for (range in ranges) {
@@ -396,14 +484,17 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix)
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) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
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 sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
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)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -412,13 +503,16 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
}
// awake phase color is transparent
val set1 = BarDataSet(values, "")
val data = BarData(set1)
// awake phase color is transparent
set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values
set1.setDrawIcons(false)
set1.isHighlightEnabled = true
//barChart.xAxis.setCenterAxisLabels(true)
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) {
@@ -441,7 +535,7 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
val duration = value.yVals[h.stackIndex].toInt()
val durationString = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration)
val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month)
val startUnix = offsetUnix + value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> acc + y.toInt() }
@@ -457,36 +551,20 @@ class LongTermStatisticsActivity : AppCompatActivity() {
override fun onNothingSelected() {}
})
val data = BarData(set1)
data.setValueTextSize(12f)
val valueCount = min(values.size, 24)
barChart.setData(data)
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate()
}
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
// Make sure the value on the bar is not out of screen.
class CustomHorizontalBarChartRenderer(chart: BarDataProvider, animator: ChartAnimator, viewPortHandler: ViewPortHandler): HorizontalBarChartRenderer(chart, animator, viewPortHandler) {
override fun drawValue(
c: Canvas,
valueText: String,
x: Float,
y: Float,
color: Int
) {
mValuePaint.setColor(color)
c.drawText(valueText, x.coerceAtLeast(60F), y, mValuePaint)
}
barChart.invalidate()
}
fun showSleepBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events)
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) {
// a sleep event can span to another day
@@ -497,14 +575,18 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix)
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) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
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 sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
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)
@@ -521,60 +603,62 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
}
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
values[index].y /= daysWithData
if (graphTypeSelection == GraphType.SLEEP_SUM) {
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
values[index].y /= daysWithData
}
}
}
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.setDrawValues(true)
set1.setDrawIcons(false)
set1.isHighlightEnabled = false
val data = BarData(set1)
data.setValueTextSize(12f)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.SLEEP_EVENTS -> value.toInt().toString()
GraphType.SLEEP_SUM -> {
prefix + DateUtils.formatTimeDuration(applicationContext, value.toLong())
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt())
}
else -> {
Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection")
prefix + value.toInt().toString()
value.toInt().toString()
}
}
}
})
val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
barChart.setData(data)
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
fun showBottleBarGraph(state: GraphState) {
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
// needed?
for (i in values.indices) {
values[i].x = i.toFloat()
}
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
state.dayCounter.setDaysWithData(event.time, event.time)
// setDaysWithData(sleepBegin, sleepEnd)
if (graphTypeSelection == GraphType.BOTTLE_EVENTS) {
values[index].y += 1F
} else if (graphTypeSelection == GraphType.BOTTLE_SUM) {
@@ -585,46 +669,62 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
}
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
values[index].y /= daysWithData
if (graphTypeSelection == GraphType.BOTTLE_SUM) {
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "index: $index, daysWithData: $daysWithData")
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
values[index].y /= daysWithData
}
}
}
val set1 = BarDataSet(values, "")
set1.setDrawValues(true)
set1.isHighlightEnabled = false
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
val data = BarData(set1)
data.setValueTextSize(12f)
//data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth.
//Log.d(TAG, "data.barWidth: ${data.barWidth}")
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.BOTTLE_EVENTS -> value.toInt().toString()
GraphType.BOTTLE_SUM -> {
prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
}
else -> {
Log.e(TAG, "unhandled graphTypeSelection")
prefix + value.toInt()
value.toInt().toString()
}
}
}
})
val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
// hm, does not work yet
data.setValueTextSize(12f)
barChart.setData(data)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.moveViewToX(values.lastOrNull()!!.x)
val maximumRange = 16F
//val count = values.size.coerceIn(5, 20)
barChart.setVisibleXRangeMaximum(maximumRange) // show max 24 entries
barChart.xAxis.setLabelCount(maximumRange.toInt(), true)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
barChart.invalidate()
}
@@ -636,8 +736,10 @@ class LongTermStatisticsActivity : AppCompatActivity() {
fun countDaysWithData(beginUnix: Long, endUnix: Long): Int {
val beginDays = unixToDays(beginUnix)
val endDays = unixToDays(endUnix)
//Log.d(TAG, "countDaysWithData: beginDays: $beginDays, endDays: $endDays, ${Date(beginUnix * 1000)} - ${Date(endUnix * 1000)}")
var count = 0
for (i in (beginDays - startDays)..<(endDays - startDays)) {
//Log.d(TAG, "countDaysWithData: i: $i, size: ${daysWithData.size}")
count += if (daysWithData[i]) { 1 } else { 0 }
}
return count
@@ -656,22 +758,8 @@ class LongTermStatisticsActivity : AppCompatActivity() {
data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int)
fun showGraph() {
barChart.fitScreen()
barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged()
barChart.clear()
barChart.invalidate()
val type = when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> LunaEvent.Type.BABY_BOTTLE
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM,
GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP
}
// wrapper for comon graph setup
fun prepareGraph(type: LunaEvent.Type, callback: (GraphState) -> Unit) {
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) {
@@ -693,7 +781,7 @@ class LongTermStatisticsActivity : AppCompatActivity() {
// days when the a day/week/month starts/ends
val startDays = unixToDays(spanToUnix(startSpan))
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next week
val dayCounter = DayCounter(startDays, endDays)
// print dates
@@ -709,38 +797,44 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val week = dateTime.get(Calendar.WEEK_OF_YEAR)
val day = dateTime.get(Calendar.DAY_OF_MONTH)
// Adjust years if the first week of a year starts in the previous year.
val years = if (month == 12 && week == 1) {
// Dirty hack to get monotone number of weeks
// The first week if the year might start in the previous year.
val yearFixed = if (month == 12 && week == 1) {
year + 1
} else {
year
}
val days = "%02d".format(day)
val weeks = "%02d".format(week)
val months = "%02d".format(month)
return when (timeRangeSelection) {
TimeRange.DAY -> "$days/$months/$years"
TimeRange.WEEK -> "$weeks/$years"
TimeRange.MONTH -> "$months/$years"
TimeRange.DAY -> "$day/$month/$yearFixed"
TimeRange.WEEK -> "$week/$yearFixed"
TimeRange.MONTH -> "$month/$yearFixed"
}
}
}
val state = GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan)
//Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
callback(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
}
fun showGraph() {
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> showBottleBarGraph(state)
GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) }
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> showSleepBarGraph(state)
GraphType.SLEEP_PATTERN -> if (timeRangeSelection == TimeRange.DAY) {
// specialized pattern bar for daily view (optional)
showSleepPatternBarGraphDaily(state)
} else {
showSleepPatternBarGraphSlotted(state)
GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) }
GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state ->
if (timeRangeSelection == TimeRange.DAY) {
// specialized pattern bar for daily view
showSleepPatternBarGraphDaily(state)
} else {
showSleepPatternBarGraphSlotted(state)
}
}
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
}
}
@@ -760,6 +854,8 @@ class LongTermStatisticsActivity : AppCompatActivity() {
val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
//Log.d(TAG, "spinner ${arrayValues.indexOf(currentValue)} (${arrayValues.joinToString { it }})")
spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@@ -767,7 +863,7 @@ class LongTermStatisticsActivity : AppCompatActivity() {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
if (pos >= arrayValues.size) {
Toast.makeText(
this@LongTermStatisticsActivity,
this@StatisticsActivity,
"pos out of bounds: $arrayValues", Toast.LENGTH_SHORT
).show()
return
@@ -784,7 +880,7 @@ class LongTermStatisticsActivity : AppCompatActivity() {
}
companion object {
const val TAG = "LongTermStatisticsActivity"
const val TAG = "StatisticsActivity"
// 15 min steps
const val SLEEP_PATTERN_GRANULARITY = 15 * 60

View File

@@ -58,12 +58,17 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
LunaEvent.Type.NOTE -> item.notes
else -> item.getRowItemTitle(context)
}
holder.time.text = DateUtils.formatTimeAgo(context, item.getEndTime())
val endTime = if (item.type == LunaEvent.Type.SLEEP) {
item.quantity + item.time
} else {
item.time
}
holder.time.text = DateUtils.formatTimeAgo(context, endTime)
var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one
if (item.type == LunaEvent.Type.WEIGHT) {
val lastWeight = getPreviousEvent(position, LunaEvent.Type.WEIGHT)
val lastWeight = getPreviousWeightEvent(position)
if (lastWeight != null) {
val differenceInWeight = item.quantity - lastWeight.quantity
val sign = if (differenceInWeight > 0) "+" else ""
@@ -74,19 +79,6 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
}
}
// if the event is height, show difference with the last one
if (item.type == LunaEvent.Type.HEIGHT) {
val lastHeight = getPreviousEvent(position, LunaEvent.Type.HEIGHT)
if (lastHeight != null) {
val differenceInHeight = item.quantity - lastHeight.quantity
val sign = if (differenceInHeight > 0) "+" else ""
quantityText += "\n($sign$differenceInHeight)"
if (differenceInHeight < 0) {
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.danger))
}
}
}
holder.quantity.text = quantityText
// Listeners
@@ -101,12 +93,12 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
return items.size
}
private fun getPreviousEvent(startFromPosition: Int, type: LunaEvent.Type): LunaEvent? {
private fun getPreviousWeightEvent(startFromPosition: Int): LunaEvent? {
if (startFromPosition == items.size - 1)
return null
for (pos in startFromPosition + 1 until items.size) {
val item = items.get(pos)
if (item.type != type)
if (item.type != LunaEvent.Type.WEIGHT)
continue
return item
}

View File

@@ -22,7 +22,6 @@ class LunaEvent: Comparable<LunaEvent> {
DIAPERCHANGE_PEE,
SLEEP,
WEIGHT,
HEIGHT,
MEDICINE,
ENEMA,
NOTE,
@@ -116,18 +115,6 @@ class LunaEvent: Comparable<LunaEvent> {
return getDialogMessage(context, type)
}
fun getStartTime(): Long {
return time
}
fun getEndTime(): Long {
return if (type == Type.SLEEP) {
time + quantity
} else {
time
}
}
fun toJson(): JSONObject {
return jo
}
@@ -146,7 +133,6 @@ class LunaEvent: Comparable<LunaEvent> {
when (type) {
Type.BABY_BOTTLE -> R.string.event_bottle_type
Type.WEIGHT -> R.string.event_weight_type
Type.HEIGHT -> R.string.event_height_type
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_type
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_type
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_type
@@ -176,8 +162,7 @@ class LunaEvent: Comparable<LunaEvent> {
Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_dialog_description
Type.WEIGHT -> R.string.log_weight_dialog_description
Type.HEIGHT -> R.string.log_height_dialog_description
Type.SLEEP -> R.string.log_duration_dialog_description
Type.SLEEP -> R.string.log_sleep_dialog_description
else -> R.string.log_unknown_dialog_description
}
)
@@ -188,7 +173,6 @@ class LunaEvent: Comparable<LunaEvent> {
when (type) {
Type.BABY_BOTTLE -> R.string.event_bottle_desc
Type.WEIGHT -> R.string.event_weight_desc
Type.HEIGHT -> R.string.event_height_desc
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_desc
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_desc
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_desc
@@ -214,7 +198,6 @@ class LunaEvent: Comparable<LunaEvent> {
when (type) {
Type.BABY_BOTTLE -> R.string.event_type_item_bottle
Type.WEIGHT -> R.string.event_type_item_weight
Type.HEIGHT -> R.string.event_type_item_height
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_type_item_breastfeeding_left
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_type_item_breastfeeding_both
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_type_item_breastfeeding_right

View File

@@ -9,15 +9,10 @@ import java.util.Date
class DateUtils {
companion object {
/**
* Format time duration in seconds as e.g. "2 hours, 1 min", rounded to minutes.
* Format time duration in seconds as e.g. "2 hours, 1 min".
* Used for the duration to the next/previous event in the event details dialog.
*/
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
val years = (seconds / (365 * 24 * 60 * 60F)).toLong()
@@ -61,11 +56,11 @@ class DateUtils {
return builder.toString()
}
if (years != 0L) {
if (years > 0) {
return format(years, days, R.string.year_ago, R.string.years_ago, R.string.day_ago, R.string.days_ago)
} else if (days != 0L) {
} else if (days > 0) {
return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago)
} else if (hours != 0L) {
} else if (hours > 0) {
return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago)
} else {
return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago)

View File

@@ -11,10 +11,10 @@ import utils.DateUtils.Companion.formatTimeDuration
import java.text.NumberFormat
class NumericUtils (val context: Context) {
val numberFormat: NumberFormat
val measurement_unit_liquid_base: String
val measurement_unit_weight_base: String
val measurement_unit_weight_tiny: String
val measurement_unit_height_base: String
val measurement_unit_temperature_base: String
private fun isMetricSystem(): Boolean {
@@ -35,6 +35,7 @@ class NumericUtils (val context: Context) {
}
init {
this.numberFormat = NumberFormat.getInstance()
this.measurement_unit_liquid_base = context.getString(
if (isMetricSystem())
R.string.measurement_unit_liquid_base_metric
@@ -53,12 +54,6 @@ class NumericUtils (val context: Context) {
else
R.string.measurement_unit_weight_tiny_imperial
)
this.measurement_unit_height_base = context.getString(
if (isMetricSystem())
R.string.measurement_unit_height_base_metric
else
R.string.measurement_unit_height_base_imperial
)
this.measurement_unit_temperature_base = context.getString(
if (isMetricSystem())
R.string.measurement_unit_temperature_base_metric
@@ -95,7 +90,6 @@ class NumericUtils (val context: Context) {
when (type) {
LunaEvent.Type.BABY_BOTTLE -> measurement_unit_liquid_base
LunaEvent.Type.WEIGHT -> measurement_unit_weight_base
LunaEvent.Type.HEIGHT -> measurement_unit_height_base
LunaEvent.Type.MEDICINE -> measurement_unit_weight_tiny
LunaEvent.Type.TEMPERATURE -> measurement_unit_temperature_base
else -> ""

View File

@@ -259,7 +259,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="10dp"
android:gravity="center_horizontal"
android:text="@string/no_connection_explain"/>

View File

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

View File

@@ -11,65 +11,46 @@
android:id="@+id/dialog_date_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="20sp"
android:text="💤"/>
<LinearLayout
android:id="@+id/duration_buttons"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<Button
android:id="@+id/dialog_date_duration_minus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-5"/>
<Button
android:id="@+id/dialog_date_duration_now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_now"/>
android:text="@string/now"/>
<Button
android:id="@+id/dialog_date_duration_clear"
android:id="@+id/dialog_date_duration_plus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_clear"/>
android:text="+5"/>
</LinearLayout>
<LinearLayout
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/dialog_date_picker_begin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
<TextView
android:id="@+id/dialog_date_range_delimiter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:text=""/>
<TextView
android:id="@+id/dialog_date_picker_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
</LinearLayout>
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
</LinearLayout>

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<EditText
android:id="@+id/dialog_number_edittext"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:inputType="number"
android:hint="0"
android:background="@drawable/textview_background"/>
<TextView
android:id="@+id/dialog_number_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="cm"/>
</LinearLayout>
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
</LinearLayout>

View File

@@ -21,7 +21,6 @@
android:background="@drawable/textview_background"/>
<TextView
android:id="@+id/dialog_number_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"

View File

@@ -13,6 +13,7 @@
<item>@string/statistics_sleep_sum</item>
<item>@string/statistics_sleep_events</item>
<item>@string/statistics_sleep_pattern</item>
<item>@string/statistics_medicine_events</item>
</string-array>
<string-array name="StatisticsTypeValues">
@@ -21,6 +22,7 @@
<item>SLEEP_SUM</item>
<item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item>
<item>MEDICINE_EVENTS</item>
</string-array>
<string-array name="StatisticsTimeLabels">

View File

@@ -7,7 +7,6 @@
<string name="event_bottle_type" translatable="false">🍼</string>
<string name="event_food_type" translatable="false">🥣</string>
<string name="event_weight_type" translatable="false">⚖️</string>
<string name="event_height_type" translatable="false">📏</string>
<string name="event_breastfeeding_left_type" translatable="false">🤱⬅️</string>
<string name="event_breastfeeding_both_type" translatable="false">🤱↔️</string>
<string name="event_breastfeeding_right_type" translatable="false">🤱➡️️</string>
@@ -27,7 +26,6 @@
<string name="event_type_item_bottle">🍼 Bottle</string>
<string name="event_type_item_food">🥣 Food</string>
<string name="event_type_item_weight">⚖️ Weight</string>
<string name="event_type_item_height">📏 Height</string>
<string name="event_type_item_breastfeeding_left">🤱⬅️ Nursing</string>
<string name="event_type_item_breastfeeding_both">🤱↔️ Nursing</string>
<string name="event_type_item_breastfeeding_right">🤱➡️️ Nursing</string>
@@ -47,7 +45,6 @@
<string name="event_bottle_desc">Milk Bottle</string>
<string name="event_food_desc">Food</string>
<string name="event_weight_desc">Weight</string>
<string name="event_height_desc">Height</string>
<string name="event_breastfeeding_left_desc">Nursing (left)</string>
<string name="event_breastfeeding_both_desc">Nursing (both)</string>
<string name="event_breastfeeding_right_desc">Nursing (right)</string>
@@ -133,17 +130,14 @@
<string name="log_temperature_dialog_description">Select the temperature:</string>
<string name="log_unknown_dialog_description"></string>
<string name="log_weight_dialog_description">Insert the weight:</string>
<string name="log_height_dialog_description">Insert the height:</string>
<string name="log_duration_dialog_description">Set duration:</string>
<string name="log_sleep_dialog_description">Set sleep duration:</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_tiny_metric" translatable="false">mg</string>
<string name="measurement_unit_height_base_metric" translatable="false">cm</string>
<string name="measurement_unit_liquid_base_imperial" translatable="false">fl oz.</string>
<string name="measurement_unit_weight_base_imperial" translatable="false">oz</string>
<string name="measurement_unit_weight_tiny_imperial" translatable="false">gr</string>
<string name="measurement_unit_height_base_imperial" translatable="false">in</string>
<string name="measurement_unit_temperature_base_imperial" translatable="false">°F</string>
<string name="measurement_unit_temperature_base_metric" translatable="false">°C</string>
@@ -166,11 +160,6 @@
<string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_duration_button_clear">Clear</string>
<string name="dialog_duration_button_minus_5">-5 min</string>
<string name="dialog_duration_button_now">Now</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_logbookname">👶 Logbook name</string>
<string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string>