From 78da94d77541cc215a7f8a0974ca2872ccce2746 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 21 Oct 2026 19:48:30 +0200 Subject: [PATCH] fix(worldclock): leave a fitting margin and drop the zone-cache reset The fitted seconds size keeps five percent spare, since hinted glyph advances do not scale exactly. The receiver no longer resets the process's default zone: that was speculative, and it changed global state from a UI flow. The settings state takes the seconds flag without a default, and a garbled KDoc sentence is fixed. --- .../clockula/data/time/AndroidClockChanges.kt | 6 ------ .../jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt | 2 +- .../jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt | 5 ++++- .../clockula/ui/worldclock/WorldClockSource.kt | 5 +++-- docs/ARCHITECTURE.md | 4 +--- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClockChanges.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClockChanges.kt index 82d49f2..2397192 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClockChanges.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClockChanges.kt @@ -10,7 +10,6 @@ import de.jeanlucmakiola.clockula.domain.time.ClockChanges import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import java.util.TimeZone import javax.inject.Inject import javax.inject.Singleton @@ -28,11 +27,6 @@ class AndroidClockChanges @Inject constructor( override fun changes(): Flow = callbackFlow { val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - // The process's cached default zone is reset by a separate - // one-way call that can land after this broadcast; resetting it - // here too (as ActivityThread does) means the rebuild this - // triggers reads the new zone, not the old one. - if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) TimeZone.setDefault(null) trySend(Unit) } } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt index a85d060..80a6372 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt @@ -15,7 +15,7 @@ internal sealed interface SettingsUiState { val defaults: ClockDefaults, val selfCheckProblemCount: Int? = null, /** The world clock's "Display time with seconds" (#23); not a clock default. */ - val worldClockSeconds: Boolean = false, + val worldClockSeconds: Boolean, ) : SettingsUiState } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt index 61a884d..31a92e5 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt @@ -175,6 +175,8 @@ internal data class RowTimeStyle(val style: TextStyle, val maxWidth: Dp) { val capped: Boolean get() = maxWidth != Dp.Infinity } +private const val FitMargin = 0.95f + /** Wide enough for "12:59:59 PM" at the default scale; the city keeps the rest. */ private val SecondsMaxWidth = 168.dp @@ -209,7 +211,8 @@ internal fun rememberRowTimeStyle(showSeconds: Boolean, formatTime: (LocalTime) val fitted = if (width <= cap) { style } else { - style.copy(fontSize = with(density) { (style.fontSize.toPx() * cap / width).toSp() }) + // A few percent spare: hinted glyph advances do not scale exactly. + style.copy(fontSize = with(density) { (style.fontSize.toPx() * FitMargin * cap / width).toSp() }) } RowTimeStyle(fitted, SecondsMaxWidth) } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt index 3a7ef95..4511f61 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt @@ -45,8 +45,9 @@ internal class WorldClockSource @Inject constructor( /** * The tab's state. Both the instant and the device zone are re-read on * **every tick**. [ClockChanges] restarts the tick, so `TIME_SET` and - * `TIMEZONE_CHANGED` show at once rather than at the next minute, the next - * wait is measured against the clock as it now reads. It sits + * `TIMEZONE_CHANGED` show at once rather than at the next minute, and the + * restarted ticker measures its next wait against the clock as it now + * reads. It sits * outside the `flatMapLatest`, so its receiver is registered once per * subscription rather than once per list edit, and `onStart` lets the * first state through without waiting for a broadcast. [refreshes] does diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 14538da..86f1f56 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2464,9 +2464,7 @@ arrive with the tab on screen (a network zone change on landing, split screen). So `ClockChanges` (`AndroidClockChanges`, a receiver registered only while the tab collects) restarts the tick: those two make the state re-read the clock and the zone at once, and the restarted ticker measures its next -wait against the clock as it now reads. On `TIMEZONE_CHANGED` the receiver -also resets the process's cached default zone itself, since the system's own -reset is a separate call that can land after the broadcast. The signal is combined +wait against the clock as it now reads. The signal is combined with the stored list, the home zone and the setting *outside* the `flatMapLatest`, so the receiver is registered once per subscription, not once per list edit. Each broadcast costs one extra `build`, which