feat(data): a transactional alarm edit, and the audio source policy
The editor and the engine write to the same row from different threads, so an edit is a transaction: read, transform, write back, with the id and the creation stamp forced from the stored row. Without it the editor would re-enable an alarm the engine had just resolved as disabled, or move a row's history. `AudioSourcePolicy` is where the silent sentinel becomes an empty source list — the audio player then fails to start by design, and the ring falls through to vibration instead of to nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
This commit is contained in:
@@ -3,35 +3,48 @@ package de.jeanlucmakiola.clockula.data.alarms
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface AlarmDao {
|
||||
abstract class AlarmDao {
|
||||
@Query("SELECT * FROM alarms ORDER BY hour, minute, id")
|
||||
fun observeAll(): Flow<List<AlarmEntity>>
|
||||
abstract fun observeAll(): Flow<List<AlarmEntity>>
|
||||
|
||||
@Query("SELECT * FROM alarms WHERE id = :id LIMIT 1")
|
||||
fun observeById(id: Long): Flow<AlarmEntity?>
|
||||
abstract fun observeById(id: Long): Flow<AlarmEntity?>
|
||||
|
||||
@Query("SELECT * FROM alarms WHERE id = :id LIMIT 1")
|
||||
suspend fun findById(id: Long): AlarmEntity?
|
||||
abstract suspend fun findById(id: Long): AlarmEntity?
|
||||
|
||||
@Query("SELECT * FROM alarms WHERE enabled = 1 ORDER BY hour, minute, id")
|
||||
suspend fun enabled(): List<AlarmEntity>
|
||||
abstract suspend fun enabled(): List<AlarmEntity>
|
||||
|
||||
@Insert
|
||||
suspend fun insert(alarm: AlarmEntity): Long
|
||||
abstract suspend fun insert(alarm: AlarmEntity): Long
|
||||
|
||||
@Update
|
||||
suspend fun update(alarm: AlarmEntity)
|
||||
abstract suspend fun update(alarm: AlarmEntity)
|
||||
|
||||
@Query("DELETE FROM alarms WHERE id = :id")
|
||||
suspend fun deleteById(id: Long)
|
||||
abstract suspend fun deleteById(id: Long)
|
||||
|
||||
@Query("UPDATE alarms SET enabled = :enabled, updated_at = :updatedAt WHERE id = :id")
|
||||
suspend fun setEnabled(id: Long, enabled: Boolean, updatedAt: Long)
|
||||
abstract suspend fun setEnabled(id: Long, enabled: Boolean, updatedAt: Long)
|
||||
|
||||
@Query("UPDATE alarms SET skip_next_occurrence = :skip, updated_at = :updatedAt WHERE id = :id")
|
||||
suspend fun setSkipNextOccurrence(id: Long, skip: Boolean, updatedAt: Long)
|
||||
abstract suspend fun setSkipNextOccurrence(id: Long, skip: Boolean, updatedAt: Long)
|
||||
|
||||
/**
|
||||
* Reads the row, applies [transform] and writes the result back inside one
|
||||
* transaction. A missing row, or a null result, writes nothing. [transform]
|
||||
* must be pure and must not touch the database.
|
||||
*/
|
||||
@Transaction
|
||||
open suspend fun updateWithin(id: Long, transform: (AlarmEntity) -> AlarmEntity?) {
|
||||
val current = findById(id) ?: return
|
||||
val next = transform(current) ?: return
|
||||
update(next)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,11 @@ interface AlarmRepository {
|
||||
suspend fun delete(id: Long)
|
||||
suspend fun setEnabled(id: Long, enabled: Boolean)
|
||||
suspend fun setSkipNextOccurrence(id: Long, skip: Boolean)
|
||||
|
||||
/**
|
||||
* Transactional read-modify-write. Stamps `updated_at` from the wall clock
|
||||
* and keeps the id, so a concurrent `setEnabled` from the engine cannot be
|
||||
* clobbered by a stale whole-row write. A missing id writes nothing.
|
||||
*/
|
||||
suspend fun edit(id: Long, transform: (Alarm) -> Alarm)
|
||||
}
|
||||
|
||||
@@ -41,4 +41,15 @@ class AlarmRepositoryImpl @Inject constructor(
|
||||
|
||||
override suspend fun setSkipNextOccurrence(id: Long, skip: Boolean) =
|
||||
dao.setSkipNextOccurrence(id, skip, wallClock.now().toEpochMilliseconds())
|
||||
|
||||
override suspend fun edit(id: Long, transform: (Alarm) -> Alarm) {
|
||||
val updatedAt = wallClock.now().toEpochMilliseconds()
|
||||
dao.updateWithin(id) { entity ->
|
||||
// The id is forced back on, so a transform can change a field but
|
||||
// never move the row; the read and the write are one transaction, so
|
||||
// a concurrent `setEnabled` from the engine is not clobbered.
|
||||
AlarmMapper.toEntity(transform(AlarmMapper.toDomain(entity)))
|
||||
.copy(id = entity.id, createdAt = entity.createdAt, updatedAt = updatedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user