forked from penguin86/luna-tracker
Fix timer edge cases: thread safety, duplicate dialogs, concurrent timers
- Remove background thread mutation of logbook.logs during merge to prevent ConcurrentModificationException crashes - Cancel ongoing timer when user enters manual duration to prevent orphaned ongoing events - Prevent duplicate timer dialogs when tapping ongoing event in list - Block starting a second timer while another is already running - Track old fingerprint on event edit to prevent merge re-adding the old version Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -334,6 +334,10 @@ class MainActivity : AppCompatActivity() {
|
||||
Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (sleepTimerStartTime > 0) {
|
||||
Toast.makeText(this, R.string.another_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Check logbook for existing ongoing breastfeeding event (from another device)
|
||||
val existingOngoing = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
|
||||
@@ -431,6 +435,11 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun askBreastfeedingDuration(eventType: String) {
|
||||
// Cancel any running timer to avoid orphaned ongoing events
|
||||
if (bfTimerType != null) {
|
||||
cancelBreastfeedingTimer()
|
||||
}
|
||||
|
||||
val d = AlertDialog.Builder(this)
|
||||
val dialogView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
|
||||
d.setTitle(R.string.breastfeeding_duration_title)
|
||||
@@ -479,6 +488,10 @@ class MainActivity : AppCompatActivity() {
|
||||
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (bfTimerType != null) {
|
||||
Toast.makeText(this, R.string.another_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Check logbook for existing ongoing sleep event (from another device)
|
||||
val existingOngoing = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
|
||||
@@ -576,6 +589,11 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun askSleepDuration() {
|
||||
// Cancel any running timer to avoid orphaned ongoing events
|
||||
if (sleepTimerStartTime > 0) {
|
||||
cancelSleepTimer()
|
||||
}
|
||||
|
||||
val d = AlertDialog.Builder(this)
|
||||
val dialogView = layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
|
||||
d.setTitle(R.string.sleep_duration_title)
|
||||
@@ -895,6 +913,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// If event is ongoing, show the appropriate timer dialog instead
|
||||
if (event.ongoing) {
|
||||
if (event.type in LunaEvent.BREASTFEEDING_TYPES) {
|
||||
if (bfTimerDialog?.isShowing == true) return
|
||||
if (bfTimerType == null) {
|
||||
bfTimerStartTime = event.time * 1000
|
||||
bfTimerType = event.type
|
||||
@@ -902,6 +921,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
showBreastfeedingTimerDialog(event.type)
|
||||
} else if (event.type == LunaEvent.TYPE_SLEEP) {
|
||||
if (sleepTimerDialog?.isShowing == true) return
|
||||
if (sleepTimerStartTime == 0L) {
|
||||
sleepTimerStartTime = event.time * 1000
|
||||
saveSleepTimerState()
|
||||
@@ -939,6 +959,8 @@ class MainActivity : AppCompatActivity() {
|
||||
TimePickerDialog(this, { _, hour, minute ->
|
||||
val pickedDateTime = Calendar.getInstance()
|
||||
pickedDateTime.set(year, month, day, hour, minute)
|
||||
// Track old fingerprint so merge doesn't re-add the old version
|
||||
logbook?.removedSinceLoad?.add(event.fingerprint())
|
||||
// Save event and move it to the right position in the logbook
|
||||
event.time = pickedDateTime.time.time / 1000 // Seconds since epoch
|
||||
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
|
||||
@@ -980,6 +1002,8 @@ class MainActivity : AppCompatActivity() {
|
||||
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val newQuantity = picker.value
|
||||
if (newQuantity != oldQuantity) {
|
||||
// Track old fingerprint so merge doesn't re-add the old version
|
||||
logbook?.removedSinceLoad?.add(event.fingerprint())
|
||||
// Adjust end time based on duration change (duration reduced = end time earlier)
|
||||
event.time = event.time - (oldQuantity - newQuantity) * 60L
|
||||
event.quantity = newQuantity
|
||||
@@ -1475,10 +1499,6 @@ class MainActivity : AppCompatActivity() {
|
||||
runOnUiThread({
|
||||
setLoading(false)
|
||||
|
||||
// Refresh list - merge may have added events from other devices
|
||||
logbook?.sort()
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
if (lastEventAdded != null)
|
||||
|
||||
@@ -133,14 +133,9 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
|
||||
}
|
||||
|
||||
private fun saveLogbook(context: Context, logbook: Logbook) {
|
||||
// Take a snapshot of local events to avoid thread safety issues
|
||||
// (UI thread may modify logbook.logs concurrently)
|
||||
val localSnapshot: List<LunaEvent>
|
||||
val removedFingerprints: Set<String>
|
||||
synchronized(logbook.logs) {
|
||||
localSnapshot = ArrayList(logbook.logs)
|
||||
removedFingerprints = HashSet(logbook.removedSinceLoad)
|
||||
}
|
||||
// Take a snapshot of local events (UI thread may modify logbook.logs concurrently)
|
||||
val localSnapshot = ArrayList(logbook.logs)
|
||||
val removedFingerprints = HashSet(logbook.removedSinceLoad)
|
||||
|
||||
// Load remote logbook and merge before saving to avoid overwriting
|
||||
// events added by other devices
|
||||
@@ -149,17 +144,9 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
|
||||
val remoteLogbook = loadLogbook(logbook.name)
|
||||
val added = mergeRemoteEvents(mergedEvents, remoteLogbook, removedFingerprints)
|
||||
if (added > 0) {
|
||||
// Add merged events back to the actual logbook for UI display
|
||||
synchronized(logbook.logs) {
|
||||
val currentFingerprints = logbook.logs.map { it.fingerprint() }.toHashSet()
|
||||
for (event in mergedEvents) {
|
||||
if (!currentFingerprints.contains(event.fingerprint())) {
|
||||
logbook.logs.add(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Merged $added remote events, saving ${mergedEvents.size} total")
|
||||
}
|
||||
// Merged events will appear in UI on next periodic load (max 60s)
|
||||
} catch (e: Exception) {
|
||||
// Remote not available (404, network error, etc.) - save local version as-is
|
||||
Log.w(TAG, "Could not load remote logbook for merge: $e")
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="breastfeeding_timer_stop">Stopp</string>
|
||||
<string name="breastfeeding_timer_hint">Tippe Stopp wenn fertig</string>
|
||||
<string name="breastfeeding_timer_already_running">Es läuft bereits eine Stillsitzung</string>
|
||||
<string name="another_timer_already_running">Es läuft bereits ein anderer Timer</string>
|
||||
<string name="breastfeeding_duration_title">Stilldauer</string>
|
||||
<string name="breastfeeding_duration_description">Dauer in Minuten eingeben</string>
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="breastfeeding_timer_stop">Arrêter</string>
|
||||
<string name="breastfeeding_timer_hint">Appuyez sur Arrêter quand terminé</string>
|
||||
<string name="breastfeeding_timer_already_running">Une session d\'allaitement est déjà en cours</string>
|
||||
<string name="another_timer_already_running">Un autre minuteur est déjà en cours</string>
|
||||
<string name="breastfeeding_duration_title">Durée d\'allaitement</string>
|
||||
<string name="breastfeeding_duration_description">Entrez la durée en minutes</string>
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="breastfeeding_timer_stop">Stop</string>
|
||||
<string name="breastfeeding_timer_hint">Premi Stop quando hai finito</string>
|
||||
<string name="breastfeeding_timer_already_running">Una sessione di allattamento è già in corso</string>
|
||||
<string name="another_timer_already_running">Un altro timer è già in corso</string>
|
||||
<string name="breastfeeding_duration_title">Durata allattamento</string>
|
||||
<string name="breastfeeding_duration_description">Inserisci la durata in minuti</string>
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
<string name="breastfeeding_timer_stop">Stop</string>
|
||||
<string name="breastfeeding_timer_hint">Tap Stop when finished</string>
|
||||
<string name="breastfeeding_timer_already_running">A breastfeeding session is already in progress</string>
|
||||
<string name="another_timer_already_running">Another timer is already running</string>
|
||||
<string name="breastfeeding_duration_title">Breastfeeding duration</string>
|
||||
<string name="breastfeeding_duration_description">Enter the duration in minutes</string>
|
||||
<string name="measurement_unit_time_minutes" translatable="false">min</string>
|
||||
|
||||
Reference in New Issue
Block a user