22 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
d832aa4330 NumericUtils: remove possible trailing whitespace 2026-02-09 08:23:38 +01:00
672ae37049 MainActivity: do not switch logbook on reload 2026-02-09 08:23:38 +01:00
672e58c028 LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2026-02-09 08:23:38 +01:00
d2729af30f 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-02-09 08:23:21 +01:00
85567cce77 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-23 06:46:11 +01:00
80a51ea8ef MainActivity: increase bottle volume to 340ml
This is the maximum amount found in sold bottles.
2026-01-23 06:46:11 +01:00
28679a4a66 gradle: use uniform implementation directive for sardine-android 2026-01-23 06:46:11 +01:00
f73d3562a9 gradle: avoid inclusion of apk signing blobs
See https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
2026-01-23 06:46:11 +01:00
8a2932b1e7 gradle: set compileSDK/targetSdk to 36 2026-01-23 06:46:11 +01:00
2af8989777 StatisticsActivity: add statistics for bottle and sleep events 2026-01-23 06:46:11 +01:00
b417fe48a6 MainActivity: show save button if any values has changed 2026-01-23 06:46:11 +01:00
a887d9f29f MainActivity: use unique templates for notes 2026-01-23 06:46:11 +01:00
44748506ff LunaEvent: add sleep event 2026-01-23 06:46:07 +01:00
12 changed files with 485 additions and 466 deletions

View File

@@ -148,19 +148,23 @@ class MainActivity : AppCompatActivity() {
val eventTypeStats = mutableMapOf<LunaEvent.Type, Int>()
if (dynamicMenu) {
val sampleSize = 100
// populate frequency map from first 100 events
allEvents.take(sampleSize.coerceAtMost(allEvents.size)).forEach {
// 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)
}
}
}
// sort all event types by frequency or ordinal
// sort all event types by frequency and 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 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)
@@ -186,7 +190,7 @@ class MainActivity : AppCompatActivity() {
fun show(vararg tvs: TextView) {
for (tv in tvs) {
val type = sortedEventTypes[showCounter]
val type = eventTypesSorted[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE
@@ -205,15 +209,8 @@ class MainActivity : AppCompatActivity() {
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
currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size)
currentPopupItems = eventTypesSorted.subList(showCounter, eventTypesSorted.size)
}
override fun onStart() {
@@ -275,8 +272,11 @@ 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 = datePickerHelper(event.time, dateTV)
val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -314,7 +314,7 @@ class MainActivity : AppCompatActivity() {
weightET.setText(event.quantity.toString())
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = datePickerHelper(event.time, dateTV)
val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -365,7 +365,7 @@ class MainActivity : AppCompatActivity() {
}
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = datePickerHelper(event.time, dateTV)
val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -389,7 +389,8 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun datePickerHelper(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
// Pick a date/time.
fun dateTimePicker(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
dateTextView.text = DateUtils.formatDateTime(time)
val dateTime = Calendar.getInstance()
@@ -420,77 +421,122 @@ class MainActivity : AppCompatActivity() {
return dateTime
}
fun addSleepEvent(event: LunaEvent) {
askSleepValue(event) { saveEvent(event) }
fun addDurationEvent(event: LunaEvent) {
askDurationEvent(event, true) { saveEvent(event) }
}
fun askSleepValue(event: LunaEvent, onPositive: () -> Unit) {
fun askDurationEvent(event: LunaEvent, showTime: 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 durationTextView = dialogView.findViewById<TextView>(R.id.dialog_date_duration)
val datePickerBegin = dialogView.findViewById<TextView>(R.id.dialog_date_picker_begin)
val datePickerEnd = dialogView.findViewById<TextView>(R.id.dialog_date_picker_end)
val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val datePicker = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
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 = durationTextView.currentTextColor
val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger)
var duration = event.quantity
// in seconds
var durationStart = event.getStartTime()
var durationEnd = event.getEndTime()
fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean {
fun isValidTime(timeUnix: Long): Boolean {
val now = System.currentTimeMillis() / 1000
return (timeSeconds + durationSeconds) <= now && durationSeconds < (24 * 60 * 60)
return timeUnix in 1..now
}
val onDateChange = { time: Long ->
durationTextView.setTextColor(currentDurationTextColor)
fun isValidTimeSpan(timeBeginUnix: Long, timeEndUnix: Long): Boolean {
return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
}
if (duration == 0) {
// baby is sleeping
fun updateFields() {
datePickerBegin.text = DateUtils.formatDateTime(durationStart)
datePickerEnd.text = DateUtils.formatDateTime(durationEnd)
durationTextView.setTextColor(currentDurationTextColor)
val duration = durationEnd - durationStart
if (duration == 0L) {
// event is ongoing
durationTextView.text = "💤"
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
} else {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
if (!isValidTime(time, duration)) {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration)
if (!isValidTimeSpan(durationStart, durationEnd)) {
durationTextView.setTextColor(invalidDurationTextColor)
}
}
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
}
val pickedDateTime = datePickerHelper(event.time, datePicker, onDateChange)
onDateChange(pickedDateTime.time.time / 1000)
fun adjust(minutes: Int) {
duration += minutes * 60
if (duration < 0) {
duration = 0
}
onDateChange(pickedDateTime.time.time / 1000)
datePickerBegin.setTextColor(if (isValidTime(durationStart)) { currentDurationTextColor } else { invalidDurationTextColor })
datePickerEnd.setTextColor(if (isValidTime(durationEnd)) { currentDurationTextColor } else { invalidDurationTextColor })
}
durationMinus5Button.setOnClickListener { adjust(-5) }
durationPlus5Button.setOnClickListener { adjust(5) }
val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { pickedTime: Long ->
durationStart = pickedTime
if (datePickerEnd.visibility == View.GONE) {
durationEnd = pickedTime
}
updateFields()
}
val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { pickedTime: Long ->
durationEnd = pickedTime
updateFields()
}
if (showTime) {
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
durationTextView.visibility = View.GONE
durationButtons.visibility = View.GONE
//d.setMessage("")
} else {
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
durationTextView.visibility = View.VISIBLE
durationButtons.visibility = View.VISIBLE
d.setMessage(event.getDialogMessage(this))
}
durationStart = pickedDateTimeBegin.time.time / 1000
durationEnd = pickedDateTimeEnd.time.time / 1000
updateFields()
durationMinus5Button.setOnClickListener {
durationEnd = (durationEnd - 300).coerceAtLeast(durationStart)
updateFields()
}
durationPlus5Button.setOnClickListener {
durationEnd = (durationEnd + 300).coerceAtLeast(durationStart)
updateFields()
}
durationClearButton.setOnClickListener {
durationEnd = durationStart
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)
}
durationEnd = System.currentTimeMillis() / 1000
updateFields()
}
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val time = pickedDateTime.time.time / 1000
if (isValidTime(time, duration)) {
event.time = time
event.quantity = duration
if (isValidTime(durationStart) && isValidTime(durationEnd) && isValidTimeSpan(durationStart, durationEnd)) {
event.time = durationStart
event.quantity = (durationEnd - durationStart).toInt()
onPositive()
} else {
Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show()
@@ -507,6 +553,7 @@ class MainActivity : AppCompatActivity() {
}
fun addAmountEvent(event: LunaEvent) {
setToPreviousQuantity(event)
askAmountValue(event, true) { saveEvent(event) }
}
@@ -523,11 +570,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 = datePickerHelper(event.time, dateTV)
val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -560,7 +607,7 @@ class MainActivity : AppCompatActivity() {
d.setView(dialogView)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedDateTime = datePickerHelper(event.time, dateTV)
val pickedDateTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -595,7 +642,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 = datePickerHelper(event.time, dateTV)
val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -807,7 +854,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.time + event.quantity)
dateEndTextView.text = DateUtils.formatDateTime(event.getEndTime())
dateEndTextView.visibility = View.VISIBLE
} else {
dateEndTextView.visibility = View.GONE
@@ -821,7 +868,7 @@ class MainActivity : AppCompatActivity() {
}
updateValues()
datePickerHelper(event.time, dateTextView, { newTime: Long ->
dateTimePicker(event.time, dateTextView, { newTime: Long ->
event.time = newTime
updateValues()
})
@@ -835,7 +882,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues)
LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues)
LunaEvent.Type.NOTE -> askNotes(event, false, updateValues)
LunaEvent.Type.SLEEP -> askSleepValue(event, updateValues)
LunaEvent.Type.SLEEP -> askDurationEvent(event, false, updateValues)
else -> {
Log.w(TAG, "Unexpected type: ${event.type}")
}
@@ -865,7 +912,7 @@ class MainActivity : AppCompatActivity() {
val previousEvent = getPreviousSameEvent(event, allEvents)
if (previousEvent != null) {
val emoji = previousEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time)
val time = DateUtils.formatTimeDuration(applicationContext, event.getStartTime() - previousEvent.getEndTime())
previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener {
alertDialog.cancel()
@@ -880,7 +927,7 @@ class MainActivity : AppCompatActivity() {
val nextEvent = getNextSameEvent(event, allEvents)
if (nextEvent != null) {
val emoji = nextEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.getStartTime() - event.getEndTime())
nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener {
alertDialog.cancel()
@@ -1131,7 +1178,7 @@ class MainActivity : AppCompatActivity() {
}
logbook?.logs?.add(0, event)
logbook?.sort()
recyclerView.adapter?.notifyItemInserted(0)
recyclerView.adapter?.notifyDataSetChanged()
recyclerView.smoothScrollToPosition(0)
saveLogbook(event)
@@ -1262,7 +1309,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addSleepEvent(event)
LunaEvent.Type.SLEEP -> addDurationEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore
}
}

View File

@@ -1,5 +1,6 @@
package it.danieleverducci.lunatracker
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.Log
@@ -11,15 +12,19 @@ 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.datasets.IBarDataSet
import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider
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
@@ -27,7 +32,6 @@ 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
@@ -47,8 +51,7 @@ class StatisticsActivity : AppCompatActivity() {
BOTTLE_SUM,
SLEEP_SUM,
SLEEP_EVENTS,
SLEEP_PATTERN,
MEDICINE_EVENTS
SLEEP_PATTERN
}
enum class TimeRange {
@@ -82,10 +85,12 @@ class StatisticsActivity : 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)
@@ -98,7 +103,7 @@ class StatisticsActivity : AppCompatActivity() {
//Log.d("event", "new value: $newValue")
newValue ?: return
graphTypeSelection = GraphType.valueOf(newValue)
updateGraph()
showGraph()
}
}
)
@@ -113,13 +118,13 @@ class StatisticsActivity : AppCompatActivity() {
newValue ?: return
timeRangeSelection = TimeRange.valueOf(newValue)
setSpans()
updateGraph()
showGraph()
}
}
)
setSpans()
updateGraph()
showGraph()
}
fun setSpans() {
@@ -136,82 +141,6 @@ class StatisticsActivity : 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> {
@@ -247,23 +176,16 @@ class StatisticsActivity : AppCompatActivity() {
return ranges
}
fun showSleepPatternBarGraph(state: GraphState) {
fun showSleepPatternBarGraphSlotted(state: GraphState) {
val ranges = toSleepRanges(state.events)
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
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)
@@ -272,15 +194,11 @@ class StatisticsActivity : AppCompatActivity() {
val sleepEnd = min(end, dayEnd)
if (sleepBegin != sleepEnd) {
//val index2 = i - spanBegin
//val duration = dayEnd - dayBegin
assert(dayBegin <= dayEnd)
assert(sleepBegin <= sleepEnd)
//val duration = sleepEnd - sleepBegin
val iBegin = (sleepBegin - dayBegin) / 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
}
}
@@ -297,17 +215,14 @@ class StatisticsActivity : 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)
@@ -317,7 +232,7 @@ class StatisticsActivity : AppCompatActivity() {
}
fun mapColor(occurrences: Int, maxOccurrences: Int): Int {
// occurences: number of reported sleeps in a specific time slice
// occurrences: number of reported sleeps in a specific time slot
// maxOccurrences: maximum number of days with data that can contribute to maxOccurrences
assert(maxOccurrences > 0)
assert(occurrences <= maxOccurrences)
@@ -334,18 +249,17 @@ class StatisticsActivity : 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: dayArray: ${dayArray.joinToString { it.toString() }}")
val vals = ArrayList<Float>()
var prevIndex = -1 // time slice index
var prevValue = -1 // number of entries we have found for time slice
var prevIndex = -1 // time slot index
var prevValue = -1 // number of entries we have found for time slot
for ((i, v) in dayArray.withIndex()) {
if (i == 0) {
prevIndex = i
prevValue = v
} else if (prevValue != v) {
vals.add((i - prevIndex).toFloat())
allColors.add(mapColor(prevValue, daysWithData))
allColors.add(mapColor(prevValue.coerceAtMost(daysWithData), daysWithData))
prevIndex = i
prevValue = v
}
@@ -356,37 +270,53 @@ class StatisticsActivity : AppCompatActivity() {
allColors.add(mapColor(prevValue, daysWithData))
}
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}")
values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
assert(values.size == index)
values.add(BarEntry(values.size.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) {
return
}
if (e != null && h != null && e.x.toInt() != -1 && h.stackIndex != -1) {
if ((lastToastShown + 3500) > System.currentTimeMillis()) {
val index = e.x.toInt()
if (index !in 0..values.size) {
return
}
val value = values[index]
if (value.yVals == null || h.stackIndex !in 0..value.yVals.size) {
return
}
if ((lastToastShown + TOAST_FREQUENCY_MS) > System.currentTimeMillis()) {
// only show one Toast message after another
return
}
val index = e.x.toInt()
val value = values[index]
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + e.x.toInt())
//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() }
val durationSeconds = SLEEP_PATTERN_GRANULARITY * value.yVals[h.stackIndex].toInt()
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index)
val startSeconds =
SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex)
.fold(0) { acc, y -> acc + y.toInt() }
val durationSeconds =
SLEEP_PATTERN_GRANULARITY * value.yVals[h.stackIndex].toInt()
val endSeconds = startSeconds + durationSeconds
val format = SimpleDateFormat("HH:mm", Locale.getDefault())
val startTimeString = format.format((dayStartUnix + startSeconds) * 1000).toString()
val endTimeString = format.format((dayStartUnix + endSeconds) * 1000).toString()
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, durationSeconds)
val startTimeString =
format.format((dayStartUnix + startSeconds) * 1000).toString()
val endTimeString =
format.format((dayStartUnix + endSeconds) * 1000).toString()
val durationString = DateUtils.formatTimeDuration(applicationContext, durationSeconds.toLong())
val daysWithData = stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY]
val daysWithDataMax = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
val daysWithData =
stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY]
val daysWithDataMax = state.dayCounter.countDaysWithData(
spanToUnix(state.startSpan + index),
spanToUnix(state.startSpan + index + 1)
)
// percentage of days in this span where baby is asleep in this time slot
val pc = if (daysWithDataMax > 0) {
@@ -396,37 +326,40 @@ class StatisticsActivity : AppCompatActivity() {
0
}
Toast.makeText(applicationContext, "$startTimeString - $endTimeString ($durationString) - ${pc}%", Toast.LENGTH_LONG).show()
Toast.makeText(
applicationContext,
"$startTimeString - $endTimeString ($durationString) - ${pc}%",
Toast.LENGTH_LONG
).show()
lastToastShown = System.currentTimeMillis()
}
}
override fun onNothingSelected() {}
})
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.colors = allColors
//set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
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.invalidate()
}
// Sleep pattern bars that do not use time slots.
// This is useful/nicer for bars that only represent data of a singular days.
fun showSleepPatternBarGraphDaily(state: GraphState) {
val ranges = toSleepRanges(state.events)
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), FloatArray(0)) })
// stack awake/sleep durations
@@ -450,7 +383,8 @@ class StatisticsActivity : AppCompatActivity() {
// update value
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) {
@@ -462,17 +396,14 @@ class StatisticsActivity : 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)
@@ -481,27 +412,36 @@ class StatisticsActivity : AppCompatActivity() {
}
}
val set1 = BarDataSet(values, "")
val data = BarData(set1)
// awake phase color is transparent
val set1 = BarDataSet(values, "")
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 && e.x.toInt() != -1 && h.stackIndex != -1) {
if ((lastToastShown + 3500) > System.currentTimeMillis()) {
if (e == null || h == null) {
return
}
val index = e.x.toInt()
if (index !in 0..values.size) {
return
}
val value = values[index]
if (value.yVals == null || h.stackIndex !in 0..value.yVals.size) {
return
}
if ((lastToastShown + TOAST_FREQUENCY_MS) > System.currentTimeMillis()) {
// only show one Toast message after another
return
}
val value = values[e.x.toInt()]
val duration = value.yVals[h.stackIndex].toInt()
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration)
val durationString = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
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() }
@@ -513,26 +453,40 @@ class StatisticsActivity : AppCompatActivity() {
Toast.makeText(applicationContext, "$startTimeString - $endTimeString ($durationString)", Toast.LENGTH_LONG).show()
lastToastShown = System.currentTimeMillis()
}
}
override fun onNothingSelected() {}
})
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
val data = BarData(set1)
data.setValueTextSize(12f)
barChart.setData(data)
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.invalidate()
}
// 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)
}
}
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
@@ -543,17 +497,14 @@ class StatisticsActivity : 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)
@@ -570,7 +521,6 @@ class StatisticsActivity : AppCompatActivity() {
}
}
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) {
@@ -579,53 +529,52 @@ class StatisticsActivity : AppCompatActivity() {
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 {
return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> value.toInt().toString()
GraphType.SLEEP_SUM -> {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt())
return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.SLEEP_SUM -> {
prefix + DateUtils.formatTimeDuration(applicationContext, value.toLong())
}
else -> {
Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection")
value.toInt().toString()
prefix + value.toInt().toString()
}
}
}
})
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
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.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) {
@@ -636,67 +585,46 @@ class StatisticsActivity : AppCompatActivity() {
}
}
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
val maximumRange = 20F
if (graphTypeSelection == GraphType.BOTTLE_SUM || graphTypeSelection == GraphType.BOTTLE_EVENTS) {
//val count = values.size.coerceIn(5, 20)
barChart.setVisibleXRangeMaximum(maximumRange) // show max 24 entries
barChart.xAxis.setLabelCount(maximumRange.toInt(), false)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
}
//val dataSets = ArrayList<IBarDataSet?>()
//dataSets.add(set1)
val data = BarData(set1)
//data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth.
//Log.d(TAG, "data.barWidth: ${data.barWidth}")
data.setValueTextSize(12f)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> value.toInt().toString()
GraphType.BOTTLE_SUM -> {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.BOTTLE_SUM -> {
prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
}
//GraphType.BOTTLE_SUM_AVERAGE -> "⌀ " + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_BABY_BOTTLE, value.toInt())
else -> {
Log.e(TAG, "unhandled graphTypeSelection")
value.toInt().toString()
prefix + value.toInt()
}
}
}
})
// hm, does not work yet
Log.d(TAG, "last value: ${values.lastOrNull()!!.x}")
barChart.moveViewToX(100F)
data.setValueTextSize(12f)
val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
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.invalidate()
}
@@ -728,8 +656,22 @@ 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)
// wrapper for comon graph setup
fun prepareGraph(type: LunaEvent.Type, cb: (GraphState) -> Unit) {
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
}
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) {
@@ -751,7 +693,7 @@ class StatisticsActivity : 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 week
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span
val dayCounter = DayCounter(startDays, endDays)
// print dates
@@ -766,29 +708,39 @@ class StatisticsActivity : AppCompatActivity() {
val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0
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) {
year + 1
} else {
year
}
val days = "%02d".format(day)
val weeks = "%02d".format(week)
val months = "%02d".format(month)
return when (timeRangeSelection) {
TimeRange.DAY -> "$day/$month/$year"
TimeRange.WEEK -> "$week/$year"
TimeRange.MONTH -> "$month/$year"
TimeRange.DAY -> "$days/$months/$years"
TimeRange.WEEK -> "$weeks/$years"
TimeRange.MONTH -> "$months/$years"
}
}
}
Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
cb(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
}
fun updateGraph() {
Log.d(TAG, "updateGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
val state = GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan)
when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) }
GraphType.BOTTLE_SUM -> showBottleBarGraph(state)
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) }
GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepPatternBarGraph(state) }
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
GraphType.SLEEP_SUM -> showSleepBarGraph(state)
GraphType.SLEEP_PATTERN -> if (timeRangeSelection == TimeRange.DAY) {
// specialized pattern bar for daily view (optional)
showSleepPatternBarGraphDaily(state)
} else {
showSleepPatternBarGraphSlotted(state)
}
}
}
@@ -808,8 +760,6 @@ class StatisticsActivity : 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 {
@@ -837,7 +787,10 @@ class StatisticsActivity : AppCompatActivity() {
const val TAG = "StatisticsActivity"
// 15 min steps
val SLEEP_PATTERN_GRANULARITY = 15 * 60
const val SLEEP_PATTERN_GRANULARITY = 15 * 60
// Time between toast messages (to prevent jams)
const val TOAST_FREQUENCY_MS = 3500
// color gradient
val SLEEP_PATTERN_COLORS = arrayOf(
@@ -846,20 +799,22 @@ class StatisticsActivity : AppCompatActivity() {
"#77B1BF".toColorInt(), "#66A7B7".toColorInt(), "#559DAF".toColorInt(), "#4493A7".toColorInt(),
"#33899F".toColorInt(), "#228097".toColorInt(), "#11768F".toColorInt(), "#006C87".toColorInt()
)
private val dateTime = Calendar.getInstance() // scratch pad
var graphTypeSelection = GraphType.SLEEP_SUM
var timeRangeSelection = TimeRange.DAY
private val dateTime = Calendar.getInstance() // scratch pad
// convert month to seconds since epoch
fun unixToMonths(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val months = dateTime.get(Calendar.MONTH)
return 12 * years + months
}
// convert month to seconds since epoch
fun monthsToUnix(months: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, months / 12)
dateTime.set(Calendar.MONTH, months % 12)
@@ -869,18 +824,25 @@ class StatisticsActivity : AppCompatActivity() {
return dateTime.time.time / 1000
}
// convert seconds to weeks since epoch
fun unixToWeeks(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val years = dateTime.get(Calendar.YEAR) - 1970
val weeks = dateTime.get(Calendar.WEEK_OF_YEAR)
val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0
if (month == 12 && weeks == 1) {
// The first week if the year might start in the previous year.
return 52 * (years + 1) + weeks
}
return 52 * years + weeks
}
// convert weeks to seconds since epoch
fun weeksToUnix(weeks: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, weeks / 52)
dateTime.set(Calendar.YEAR, 1970 + weeks / 52)
dateTime.set(Calendar.WEEK_OF_YEAR, weeks % 52)
dateTime.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
dateTime.set(Calendar.HOUR, 0)
@@ -889,17 +851,16 @@ class StatisticsActivity : AppCompatActivity() {
return dateTime.time.time / 1000
}
// convert seconds to days since epoch
fun unixToDays(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val days = dateTime.get(Calendar.DAY_OF_YEAR)
return 365 * years + days
}
// convert from days to Date
// convert days to seconds since epoch
fun daysToUnix(days: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, days / 365)
dateTime.set(Calendar.DAY_OF_YEAR, days % 365)
@@ -923,42 +884,6 @@ class StatisticsActivity : AppCompatActivity() {
return newArray
}
/*
fun colorGradient(fromColor: Int, toColor: Int, percent: Int): Int {
assert(percent in 0..100)
val a1 = fromColor.shr(24).and(0xff)
val r1 = fromColor.shr(16).and(0xff)
val g1 = fromColor.shr(8).and(0xff)
val b1 = fromColor.shr(0).and(0xff)
//Log.d(TAG, "${a1.toHexString()} ${r1.toHexString()} ${g1.toHexString()} ${b1.toHexString()}")
val a2 = toColor.shr(24).and(0xff)
val r2 = toColor.shr(16).and(0xff)
val g2 = toColor.shr(8).and(0xff)
val b2 = toColor.shr(0).and(0xff)
//Log.d(TAG, "${a2.toHexString()} ${r2.toHexString()} ${g2.toHexString()} ${b2.toHexString()}")
val pc = (percent.toFloat() / 100F).coerceIn(0F, 1F)
val a = a1 + (pc * abs(a2 - a1)).toInt()
val r = r1 + (pc * abs(r2 - r1)).toInt()
val g = g1 + (pc * abs(g2 - g1)).toInt()
val b = a1 + (pc * abs(b2 - b1)).toInt()
//Log.d(TAG, "${a.toHexString()} ${r.toHexString()} ${g.toHexString()} ${b.toHexString()}")
val Red = r.shl(16).and(0x00FF0000)
val Green = g.shl(8).and(0x0000FF00)
val Blue = b.and(0x000000FF)
val aa = a.shl(24).and(0xFF000000.toInt())
val color = aa.or(Red).or(Green).or(Blue)
return color
//Log.d(TAG, "c: ${c.toHexString()} ${color.toInt().toHexString()}")
//return Color.argb(a, r, g, b)
}
*/
// for debugging
fun debugBarValues(values: ArrayList<BarEntry>) {
for (value in values) {

View File

@@ -58,12 +58,7 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
LunaEvent.Type.NOTE -> item.notes
else -> item.getRowItemTitle(context)
}
val endTime = if (item.type == LunaEvent.Type.SLEEP) {
item.quantity + item.time
} else {
item.time
}
holder.time.text = DateUtils.formatTimeAgo(context, endTime)
holder.time.text = DateUtils.formatTimeAgo(context, item.getEndTime())
var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one

View File

@@ -115,6 +115,18 @@ 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
}
@@ -162,7 +174,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.SLEEP -> R.string.log_sleep_dialog_description
Type.SLEEP -> R.string.log_duration_dialog_description
else -> R.string.log_unknown_dialog_description
}
)

View File

@@ -9,10 +9,15 @@ import java.util.Date
class DateUtils {
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.
*/
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()
@@ -56,11 +61,11 @@ class DateUtils {
return builder.toString()
}
if (years > 0) {
if (years != 0L) {
return format(years, days, R.string.year_ago, R.string.years_ago, R.string.day_ago, R.string.days_ago)
} else if (days > 0) {
} else if (days != 0L) {
return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago)
} else if (hours > 0) {
} else if (hours != 0L) {
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

@@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@@ -177,15 +177,16 @@
android:gravity="center_horizontal"
android:textSize="30sp"/>
<ImageView
<TextView
android:id="@+id/button_more"
android:layout_width="60dp"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="0"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:src="@drawable/ic_more"
android:textSize="30sp"
android:text="☰"
app:tint="@android:color/darker_gray"/>
</LinearLayout>

View File

@@ -21,7 +21,7 @@
android:layout_height="match_parent"
android:visibility="gone"
android:gravity="center"
android:text="No Data"/>
android:text="@string/statistics_no_data"/>
</FrameLayout>

View File

@@ -18,6 +18,7 @@
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,12 +11,14 @@
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:layout_width="match_parent"
android:id="@+id/duration_buttons"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
@@ -27,29 +29,61 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-5"/>
android:text="@string/dialog_duration_button_minus_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/now"/>
android:text="@string/dialog_duration_button_now"/>
<Button
android:id="@+id/dialog_date_duration_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_clear"/>
<Button
android:id="@+id/dialog_date_duration_plus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="+5"/>
android:text="@string/dialog_duration_button_plus_5"/>
</LinearLayout>
<LinearLayout
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"
android:id="@+id/dialog_date_picker_begin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
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>
</LinearLayout>

View File

@@ -13,7 +13,6 @@
<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">
@@ -22,7 +21,6 @@
<item>SLEEP_SUM</item>
<item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item>
<item>MEDICINE_EVENTS</item>
</string-array>
<string-array name="StatisticsTimeLabels">

View File

@@ -89,6 +89,7 @@
<string name="no_connection_retry">Retry</string>
<string name="statistics_title">Statistics</string>
<string name="statistics_no_data">No Data</string>
<string name="settings_dynamic_menu">Dynamic Menu</string>
<string name="settings_dynamic_menu_desc">Populate the header menu with the most used events.</string>
@@ -129,7 +130,7 @@
<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_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_weight_base_metric" translatable="false">g</string>
@@ -159,6 +160,11 @@
<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>