fix(tiles): follow the quick-timer tile's whole model, and clear stranded timers
WidgetSync now redraws the quick-timer tile on its own model rather than on the timers alone, so a duration restored from a backup reaches it too and no writer has to ask for a redraw by hand. A transient timer the tile created but never got running is deleted on the next tap instead of sitting idle in the Timers list for good. The tile model loses its unused action enum: an Active supertype carries the timer id every reset needs.
This commit is contained in:
1 parent
beeca29f74
commit
3b488ef6fc
6 files changed
+57
-44
No files matched your search
@@ -17,37 +17,28 @@ import kotlin.time.Instant
|
||||
*/
|
||||
sealed interface QuickTimerTileModel {
|
||||
|
||||
/** What a tap does now. */
|
||||
val action: QuickTimerTileAction
|
||||
/** Offering to start a timer of [duration]; a tap starts one. */
|
||||
data class AtRest(val duration: Duration) : QuickTimerTileModel
|
||||
|
||||
/** Offering to start a timer of [duration]. */
|
||||
data class AtRest(val duration: Duration) : QuickTimerTileModel {
|
||||
override val action get() = QuickTimerTileAction.START
|
||||
/**
|
||||
* Its timer [timerId] active. A tap resets it — which, for a transient
|
||||
* timer, stops it (or its ring) and deletes it, as the widget's second
|
||||
* tap does.
|
||||
*/
|
||||
sealed interface Active : QuickTimerTileModel {
|
||||
val timerId: Long
|
||||
}
|
||||
|
||||
/** Its timer counting down, due at [endsAt]. */
|
||||
data class Running(val timerId: Long, val endsAt: Instant) : QuickTimerTileModel {
|
||||
override val action get() = QuickTimerTileAction.RESET
|
||||
}
|
||||
data class Running(override val timerId: Long, val endsAt: Instant) : Active
|
||||
|
||||
/** Its timer paused with [remaining] left. */
|
||||
data class Paused(val timerId: Long, val remaining: Duration) : QuickTimerTileModel {
|
||||
override val action get() = QuickTimerTileAction.RESET
|
||||
}
|
||||
data class Paused(override val timerId: Long, val remaining: Duration) : Active
|
||||
|
||||
/** Its timer ringing. */
|
||||
data class Expired(val timerId: Long) : QuickTimerTileModel {
|
||||
override val action get() = QuickTimerTileAction.RESET
|
||||
}
|
||||
data class Expired(override val timerId: Long) : Active
|
||||
}
|
||||
|
||||
/**
|
||||
* A tap at rest starts; a tap on an active timer resets it — which, for a
|
||||
* transient timer, stops it (or its ring) and deletes it, exactly as the
|
||||
* widget's second tap does.
|
||||
*/
|
||||
enum class QuickTimerTileAction { START, RESET }
|
||||
|
||||
object QuickTimerTilePresenter {
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import de.jeanlucmakiola.clockula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.clockula.data.timers.QuickTileStateStore
|
||||
import de.jeanlucmakiola.clockula.data.timers.TimerRepository
|
||||
import de.jeanlucmakiola.clockula.domain.TimerDraft
|
||||
import de.jeanlucmakiola.clockula.domain.TimerState
|
||||
import de.jeanlucmakiola.clockula.domain.tile.QuickTimerTileModel
|
||||
import de.jeanlucmakiola.clockula.domain.tile.QuickTimerTilePresenter
|
||||
import de.jeanlucmakiola.clockula.domain.time.ElapsedRealtimeClock
|
||||
@@ -55,6 +56,7 @@ class QuickTileTimer @Inject constructor(
|
||||
suspend fun onTap(): Unit = mutex.withLock {
|
||||
when (val current = model.first()) {
|
||||
is QuickTimerTileModel.AtRest -> {
|
||||
dropStrandedTimer()
|
||||
val id = timers.create(TimerDraft(duration = current.duration, deleteAfterUse = true))
|
||||
// Remembered before it starts, so a start that throws, or a
|
||||
// process that dies in between, never leaves a running timer
|
||||
@@ -63,12 +65,22 @@ class QuickTileTimer @Inject constructor(
|
||||
state.setTimerId(id)
|
||||
engine.start(id)
|
||||
}
|
||||
is QuickTimerTileModel.Running -> reset(current.timerId)
|
||||
is QuickTimerTileModel.Paused -> reset(current.timerId)
|
||||
is QuickTimerTileModel.Expired -> reset(current.timerId)
|
||||
is QuickTimerTileModel.Active -> reset(current.timerId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A transient timer the tile created but never got running — a start that
|
||||
* threw, or a process that died in between — would sit idle in the Timers
|
||||
* list for good, since only a reset deletes one and a reset of an idle
|
||||
* timer does nothing. The next tap clears it before making another.
|
||||
*/
|
||||
private suspend fun dropStrandedTimer() {
|
||||
val id = state.currentTimerId() ?: return
|
||||
val timer = timers.find(id) ?: return
|
||||
if (timer.deleteAfterUse && timer.state == TimerState.IDLE) engine.delete(id)
|
||||
}
|
||||
|
||||
private suspend fun reset(timerId: Long) {
|
||||
engine.reset(timerId)
|
||||
state.setTimerId(null)
|
||||
|
||||
@@ -22,8 +22,6 @@ import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
||||
import de.jeanlucmakiola.clockula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.clockula.tile.NextAlarmTileService
|
||||
import de.jeanlucmakiola.clockula.tile.QuickTimerTileService
|
||||
import de.jeanlucmakiola.clockula.tile.requestTileUpdate
|
||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaTheme
|
||||
import de.jeanlucmakiola.clockula.ui.widget.config.QuickTimerPicker
|
||||
import de.jeanlucmakiola.clockula.ui.widget.openAppIntent
|
||||
@@ -56,8 +54,8 @@ class TilePreferencesActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Exported for System UI, so anything else that starts it explicitly
|
||||
// is turned away rather than shown a settings screen.
|
||||
// Answers only the action System UI sends. Not access control —
|
||||
// nothing behind it needs any: it is one setting, changed by a tap.
|
||||
if (intent?.action != TileService.ACTION_QS_TILE_PREFERENCES) {
|
||||
finish()
|
||||
return
|
||||
@@ -98,8 +96,6 @@ class TilePreferencesActivity : ComponentActivity() {
|
||||
saving = true
|
||||
lifecycleScope.launch {
|
||||
withContext(NonCancellable) { settingsPrefs.setQuickTileDuration(duration) }
|
||||
// The one write to the tile's state that `WidgetSync` doesn't watch.
|
||||
requestTileUpdate(this@TilePreferencesActivity, QuickTimerTileService::class.java)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository
|
||||
import de.jeanlucmakiola.clockula.tile.NextAlarmTileService
|
||||
import de.jeanlucmakiola.clockula.tile.QuickTimerTileService
|
||||
import de.jeanlucmakiola.clockula.tile.requestTileUpdate
|
||||
import de.jeanlucmakiola.clockula.timer.QuickTileTimer
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -39,12 +40,11 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
* (`widget/QuickTimerWidget.kt`). The analog clock reads none of them; its
|
||||
* date is kept current by [MidnightRefresh].
|
||||
*
|
||||
* The two Quick Settings tiles (#26) ride the same collectors: both are
|
||||
* *active* tiles, so nothing redraws them outside a click unless the app asks
|
||||
* the system to bind them, which is what a change here does. The quick-timer
|
||||
* tile follows the timers, as the quick-timer widget does; its duration is
|
||||
* written only by `ui/tile/TilePreferencesActivity.kt`, which asks for its
|
||||
* own redraw.
|
||||
* The two Quick Settings tiles (#26) are kept here too: both are *active*
|
||||
* tiles, so nothing redraws them outside a click unless the app asks the
|
||||
* system to bind them, which is what a change here does. The next-alarm tile
|
||||
* rides the next-alarm widget's flow; the quick-timer tile follows its own
|
||||
* model, which covers its timer, its remembered id and its duration.
|
||||
*
|
||||
* Started once, from [de.jeanlucmakiola.clockula.ClockulaApp.onCreate], in the
|
||||
* app's `@ApplicationScope` — never driven by a `Ticker`, so an idle home
|
||||
@@ -58,11 +58,15 @@ class WidgetSync @Inject constructor(
|
||||
private val timers: TimerRepository,
|
||||
private val stopwatch: StopwatchRepository,
|
||||
private val worldClocks: WorldClockRepository,
|
||||
private val quickTile: QuickTileTimer,
|
||||
) {
|
||||
|
||||
suspend fun run(): Unit = coroutineScope {
|
||||
sync(alarmEngine.upcoming(), NextAlarmWidget(), tiles = listOf(NextAlarmTileService::class.java))
|
||||
sync(timers.timers(), TimerWidget(), QuickTimerWidget(), tiles = listOf(QuickTimerTileService::class.java))
|
||||
sync(timers.timers(), TimerWidget(), QuickTimerWidget())
|
||||
// The tile's own model rather than the timers alone: its duration
|
||||
// (a pick, or a restored backup) and its timer id are part of it too.
|
||||
sync(quickTile.model, tiles = listOf(QuickTimerTileService::class.java))
|
||||
// A lap leaves the run untouched, so the lap count is watched too.
|
||||
sync(combine(stopwatch.run(), stopwatch.laps()) { run, laps -> run to laps.size }, StopwatchWidget())
|
||||
sync(worldClocks.worldClocks(), ClockWidget())
|
||||
|
||||
@@ -14,11 +14,10 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class QuickTimerTileModelTest {
|
||||
|
||||
@Test
|
||||
fun `nothing started is at rest on its duration, and a tap starts`() {
|
||||
fun `nothing started is at rest on its duration`() {
|
||||
val model = QuickTimerTilePresenter.present(5.minutes, null, listOf(runningTimerWith(id = 1, left = 1.minutes)), Duration.ZERO, T0)
|
||||
|
||||
assertThat(model).isEqualTo(QuickTimerTileModel.AtRest(5.minutes))
|
||||
assertThat(model.action).isEqualTo(QuickTimerTileAction.START)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -28,23 +27,20 @@ class QuickTimerTileModelTest {
|
||||
val model = QuickTimerTilePresenter.present(5.minutes, 2L, timers, Duration.ZERO, T0)
|
||||
|
||||
assertThat(model).isEqualTo(QuickTimerTileModel.Running(2L, T0 + 90.seconds))
|
||||
assertThat(model.action).isEqualTo(QuickTimerTileAction.RESET)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `paused shows what is left, and a tap resets`() {
|
||||
fun `paused shows what is left`() {
|
||||
val model = QuickTimerTilePresenter.present(5.minutes, 1L, listOf(pausedTimer(id = 1, remaining = 2.minutes)), Duration.ZERO, T0)
|
||||
|
||||
assertThat(model).isEqualTo(QuickTimerTileModel.Paused(1L, 2.minutes))
|
||||
assertThat(model.action).isEqualTo(QuickTimerTileAction.RESET)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expired is done, and a tap resets`() {
|
||||
fun `expired is done`() {
|
||||
val model = QuickTimerTilePresenter.present(5.minutes, 1L, listOf(expiredTimer(id = 1)), Duration.ZERO, T0)
|
||||
|
||||
assertThat(model).isEqualTo(QuickTimerTileModel.Expired(1L))
|
||||
assertThat(model.action).isEqualTo(QuickTimerTileAction.RESET)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -74,6 +74,20 @@ class QuickTileTimerTest {
|
||||
assertThat(second.state).isEqualTo(TimerState.RUNNING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a transient timer it created but never started is cleared by the next tap`(@TempDir tempDir: Path) = runTest {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
val (tile, state) = harness.quickTile()
|
||||
val stranded = harness.timers.create(TimerDraft(duration = 5.minutes, deleteAfterUse = true))
|
||||
state.setTimerId(stranded)
|
||||
|
||||
tile.onTap()
|
||||
|
||||
val left = harness.timers.timers().first().single()
|
||||
assertThat(left.id).isNotEqualTo(stranded)
|
||||
assertThat(left.state).isEqualTo(TimerState.RUNNING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two taps at once start one timer and reset it, never two`(@TempDir tempDir: Path) = runTest {
|
||||
val harness = timerEngineHarness(tempDir)
|
||||
|
||||
Reference in new issue
Block a user