diff --git a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt index 9676733..4d2e425 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt @@ -329,18 +329,34 @@ class MainActivity : AppCompatActivity() { } fun startBreastfeedingTimer(eventType: String) { - // Check if timer already running + // Check if timer already running locally if (bfTimerType != null) { Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show() return } - // Save timer state - bfTimerStartTime = System.currentTimeMillis() + // Check logbook for existing ongoing breastfeeding event (from another device) + val existingOngoing = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) } + if (existingOngoing != null) { + bfTimerStartTime = existingOngoing.time * 1000 + bfTimerType = existingOngoing.type + saveBreastfeedingTimerState() + showBreastfeedingTimerDialog(existingOngoing.type) + return + } + + // Create ongoing event and save to logbook (syncs via WebDAV) + val event = LunaEvent(eventType) + event.ongoing = true + event.signature = signature + bfTimerStartTime = event.time * 1000 bfTimerType = eventType saveBreastfeedingTimerState() - // Show timer dialog + logbook?.logs?.add(0, event) + recyclerView.adapter?.notifyItemInserted(0) + saveLogbook() + showBreastfeedingTimerDialog(eventType) } @@ -383,13 +399,19 @@ class MainActivity : AppCompatActivity() { fun stopBreastfeedingTimer() { bfTimerHandler?.removeCallbacks(bfTimerRunnable!!) - val durationMillis = System.currentTimeMillis() - bfTimerStartTime - val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute - + val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) } val eventType = bfTimerType clearBreastfeedingTimerState() - if (eventType != null) { + if (ongoingEvent != null) { + ongoingEvent.finalizeOngoing(System.currentTimeMillis()) + logbook?.sort() + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } else if (eventType != null) { + // Fallback: no ongoing event found (sync issue), create event the old way + val durationMillis = System.currentTimeMillis() - bfTimerStartTime + val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) logEvent(LunaEvent(eventType, durationMinutes)) } } @@ -397,6 +419,14 @@ class MainActivity : AppCompatActivity() { fun cancelBreastfeedingTimer() { bfTimerHandler?.removeCallbacks(bfTimerRunnable!!) clearBreastfeedingTimerState() + + // Remove ongoing event from logbook + val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) } + if (ongoingEvent != null) { + logbook?.logs?.remove(ongoingEvent) + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } } fun askBreastfeedingDuration(eventType: String) { @@ -443,17 +473,32 @@ class MainActivity : AppCompatActivity() { // Sleep timer methods fun startSleepTimer() { - // Check if timer already running + // Check if timer already running locally if (sleepTimerStartTime > 0) { Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show() return } - // Save timer state - sleepTimerStartTime = System.currentTimeMillis() + // Check logbook for existing ongoing sleep event (from another device) + val existingOngoing = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) } + if (existingOngoing != null) { + sleepTimerStartTime = existingOngoing.time * 1000 + saveSleepTimerState() + showSleepTimerDialog() + return + } + + // Create ongoing event and save to logbook (syncs via WebDAV) + val event = LunaEvent(LunaEvent.TYPE_SLEEP) + event.ongoing = true + event.signature = signature + sleepTimerStartTime = event.time * 1000 saveSleepTimerState() - // Show timer dialog + logbook?.logs?.add(0, event) + recyclerView.adapter?.notifyItemInserted(0) + saveLogbook() + showSleepTimerDialog() } @@ -499,17 +544,33 @@ class MainActivity : AppCompatActivity() { fun stopSleepTimer() { sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!) - val durationMillis = System.currentTimeMillis() - sleepTimerStartTime - val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute - + val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) } clearSleepTimerState() - logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes)) + if (ongoingEvent != null) { + ongoingEvent.finalizeOngoing(System.currentTimeMillis()) + logbook?.sort() + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } else { + // Fallback: no ongoing event found (sync issue), create event the old way + val durationMillis = System.currentTimeMillis() - sleepTimerStartTime + val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) + logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes)) + } } fun cancelSleepTimer() { sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!) clearSleepTimerState() + + // Remove ongoing event from logbook + val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) } + if (ongoingEvent != null) { + logbook?.logs?.remove(ongoingEvent) + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } } fun askSleepDuration() { @@ -829,6 +890,25 @@ class MainActivity : AppCompatActivity() { } fun showEventDetailDialog(event: LunaEvent, items: ArrayList) { + // If event is ongoing, show the appropriate timer dialog instead + if (event.ongoing) { + if (event.type in LunaEvent.BREASTFEEDING_TYPES) { + if (bfTimerType == null) { + bfTimerStartTime = event.time * 1000 + bfTimerType = event.type + saveBreastfeedingTimerState() + } + showBreastfeedingTimerDialog(event.type) + } else if (event.type == LunaEvent.TYPE_SLEEP) { + if (sleepTimerStartTime == 0L) { + sleepTimerStartTime = event.time * 1000 + saveSleepTimerState() + } + showSleepTimerDialog() + } + return + } + // Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user pauseLogbookUpdate = true val d = AlertDialog.Builder(this) @@ -1225,6 +1305,7 @@ class MainActivity : AppCompatActivity() { findViewById(R.id.no_connection_screen).visibility = View.GONE logbook = lb showLogbook() + checkForOngoingEvents() if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) { for (e in logbook?.logs ?: listOf()) { @@ -1283,6 +1364,59 @@ class MainActivity : AppCompatActivity() { }) } + fun checkForOngoingEvents() { + val logs = logbook?.logs ?: return + val now = System.currentTimeMillis() / 1000 + val maxOngoingSeconds = 24 * 60 * 60 // 24 hours + + // Check for stale ongoing events (>24h) + for (event in logs.toList()) { + if (event.ongoing && (now - event.time) > maxOngoingSeconds) { + showStaleTimerDialog(event) + return + } + } + + // Check for ongoing breastfeeding from another device + if (bfTimerType == null) { + val ongoingBf = LunaEvent.findOngoingBreastfeeding(logs) + if (ongoingBf != null) { + bfTimerStartTime = ongoingBf.time * 1000 + bfTimerType = ongoingBf.type + saveBreastfeedingTimerState() + showBreastfeedingTimerDialog(ongoingBf.type) + } + } + + // Check for ongoing sleep from another device + if (sleepTimerStartTime == 0L) { + val ongoingSleep = LunaEvent.findOngoing(logs, LunaEvent.TYPE_SLEEP) + if (ongoingSleep != null) { + sleepTimerStartTime = ongoingSleep.time * 1000 + saveSleepTimerState() + showSleepTimerDialog() + } + } + } + + fun showStaleTimerDialog(event: LunaEvent) { + val d = AlertDialog.Builder(this) + d.setTitle(R.string.stale_timer_title) + d.setMessage(R.string.stale_timer_message) + d.setPositiveButton(R.string.stale_timer_finalize) { _, _ -> + event.finalizeOngoing(System.currentTimeMillis()) + logbook?.sort() + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } + d.setNegativeButton(R.string.stale_timer_delete) { _, _ -> + logbook?.logs?.remove(event) + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } + d.create().show() + } + fun logEvent(event: LunaEvent) { savingEvent(true) diff --git a/app/src/main/java/it/danieleverducci/lunatracker/adapters/LunaEventRecyclerAdapter.kt b/app/src/main/java/it/danieleverducci/lunatracker/adapters/LunaEventRecyclerAdapter.kt index 409471f..9d27989 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/adapters/LunaEventRecyclerAdapter.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/adapters/LunaEventRecyclerAdapter.kt @@ -59,6 +59,21 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter item.notes else -> item.getTypeDescription(context) } + if (item.ongoing) { + // Show ongoing timer info + val elapsedSeconds = (System.currentTimeMillis() / 1000) - item.time + val minutes = elapsedSeconds / 60 + val hours = minutes / 60 + holder.time.text = if (hours > 0) { + String.format("%dh %02dm", hours, minutes % 60) + } else { + String.format("%d min", minutes) + } + holder.quantity.text = context.getString(R.string.event_ongoing) + holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.accent)) + return + } + holder.time.text = DateUtils.formatTimeAgo(context, item.time) var quantityText = numericUtils.formatEventQuantity(item) diff --git a/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt b/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt index 1de111b..bd359fc 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt @@ -31,6 +31,20 @@ class LunaEvent: Comparable { const val TYPE_PUKE = "PUKE" const val TYPE_BATH = "BATH" const val TYPE_SLEEP = "SLEEP" + + val BREASTFEEDING_TYPES = listOf( + TYPE_BREASTFEEDING_LEFT_NIPPLE, + TYPE_BREASTFEEDING_BOTH_NIPPLE, + TYPE_BREASTFEEDING_RIGHT_NIPPLE + ) + + fun findOngoing(events: List, type: String): LunaEvent? { + return events.firstOrNull { it.ongoing && it.type == type } + } + + fun findOngoingBreastfeeding(events: List): LunaEvent? { + return events.firstOrNull { it.ongoing && it.type in BREASTFEEDING_TYPES } + } } private val jo: JSONObject @@ -62,6 +76,14 @@ class LunaEvent: Comparable { if (value.isNotEmpty()) jo.put("signature", value) } + var ongoing: Boolean + get() = jo.optInt("ongoing", 0) == 1 + set(value) { + if (value) + jo.put("ongoing", 1) + else + jo.remove("ongoing") + } constructor(jo: JSONObject) { this.jo = jo @@ -138,6 +160,16 @@ class LunaEvent: Comparable { } } + fun finalizeOngoing(endTimeMillis: Long) { + if (!ongoing) return + val startTimeSeconds = this.time + val endTimeSeconds = endTimeMillis / 1000 + val durationMinutes = Math.max(1, ((endTimeSeconds - startTimeSeconds) / 60).toInt()) + this.time = endTimeSeconds + this.quantity = durationMinutes + this.ongoing = false + } + fun toJson(): JSONObject { return jo } diff --git a/app/src/main/java/utils/StatisticsCalculator.kt b/app/src/main/java/utils/StatisticsCalculator.kt index aed0964..3923192 100644 --- a/app/src/main/java/utils/StatisticsCalculator.kt +++ b/app/src/main/java/utils/StatisticsCalculator.kt @@ -79,14 +79,14 @@ class StatisticsCalculator(private val events: List) { } private fun getEventsInRange(startUnix: Long, endUnix: Long): List { - return events.filter { it.time >= startUnix && it.time < endUnix } + return events.filter { it.time >= startUnix && it.time < endUnix && !it.ongoing } } private fun getEventsForDays(days: Int): List { val now = System.currentTimeMillis() / 1000 val startOfToday = getStartOfDay(now) val startTime = startOfToday - (days - 1) * 24 * 60 * 60 - return events.filter { it.time >= startTime } + return events.filter { it.time >= startTime && !it.ongoing } } /** diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 30a691a..3e509d1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -236,6 +236,13 @@ %d Ereignisse importiert Import fehlgeschlagen + + läuft… + Abgelaufener Timer + Ein Timer läuft seit über 24 Stunden. Wurde er vergessen? + Mit aktueller Dauer speichern + Löschen + Logbook bearbeiten Logbook-Name diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bf5fa87..77d940a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -204,6 +204,13 @@ %d événements importés Échec de l\'import + + en cours… + Minuterie expirée + Une minuterie est active depuis plus de 24 heures. A-t-elle été oubliée ? + Enregistrer avec la durée actuelle + Supprimer + Modifier le journal Nom du journal diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6843b0b..16fe674 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -204,6 +204,13 @@ %d eventi importati Importazione fallita + + in corso… + Timer scaduto + Un timer è attivo da oltre 24 ore. È stato dimenticato? + Salva con la durata attuale + Elimina + Modifica diario Nome del diario diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9564171..7e16373 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -262,6 +262,13 @@ Imported %d events Import failed + + running… + Expired timer + A timer has been running for more than 24 hours. Was it forgotten? + Save with current duration + Delete + Edit Logbook Logbook name