diff options
| author | 2025-03-05 18:53:57 +0000 | |
|---|---|---|
| committer | 2025-03-07 08:30:49 -0800 | |
| commit | d88a2b3301a33217459480d6f12a6fc3504defa4 (patch) | |
| tree | 14f9d4d2247bc5553f630c01e172adb292884e59 | |
| parent | 91a4647e4f1048c00e93e9b3a0c85de5d81ec107 (diff) | |
[SB][Chips] Allow timer chips to count down to a time in the future.
The current timer chip implementation only counts up from a time in the
past, and it shows 00:00 if the time is in the future. But apps could
also set their notification time to be in the future and set
`isCountDown=true`, meaning that they want the notification UI to count
down to that future time.
This CL implements that behavior for both non-Compose chips and Compose
chips. I don't expect a lot of apps to use this feature, but we should
have it in case. The Compose implementation is slightly better (if the
duration would be negative, we hide it), but the non-Compose
implementation works just like normal notifications so I think it's okay
since we plan to launch the Compose chips at the same time as the
notification chips.
Fixes: 400786236
Fixes: 401464026
Bug: 364653005
Flag: com.android.systemui.status_bar_notification_chips
Test: Post notif with `when` 15 seconds in the future,
showsChronomter=true, isCountDown=true ->
- ChipMod on: chip counts down from 00:15, 00:14, ... 00:00,
then hides the text.
- ChipMod off: chip counts down from 00:15, 00:14, ... 00:00 ->
-00:01, -00:02, ...
Test: Post notif with `when` 15 seconds in the future,
showsChronometer=true, isCountDown=false ->
- ChipMod on: chip shows nothing for 15 seconds, then starts showing
00:00, 00:01, etc.
- ChipMod off: chip show -00:15, -00:14, ... 00:00, 00:01 ...
Test: Post notif with `when` 15 seconds in the past,
showChronometer=true, isCountDown=true ->
- ChipMod on: chip never shows any text
- ChipMod off: chip shows -00:15, -00:16, ...
Test: Post notif with `when` 15 seconds in the past,
showChronometer=true, isCountDown=false ->
- ChipMod on: chip starts at 00:15 and counts up with 00:16, 00:17...
- ChipMod off: chip starts at 00:15 and counts up with 00:16, 00:17...
Test: atest ChronometerStateTest NotifChipsViewModelTest
Test: Test the above with ChipsModernization both off and on
Change-Id: I20d1cf44e5ae2937ac64ceacb7ea045b9fe9989d
8 files changed, 263 insertions, 53 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 4993b5661373..b5cfc7e9080d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -573,6 +573,8 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest!![0] as OngoingActivityChipModel.Active.Timer).startTimeMs) .isEqualTo(whenElapsed) + assertThat((latest!![0] as OngoingActivityChipModel.Active.Timer).isEventInFuture) + .isFalse() } @Test @@ -608,6 +610,8 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest!![0] as OngoingActivityChipModel.Active.Timer).startTimeMs) .isEqualTo(whenElapsed) + assertThat((latest!![0] as OngoingActivityChipModel.Active.Timer).isEventInFuture) + .isTrue() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerStateTest.kt index 4e92540396d3..cd9970cfa614 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerStateTest.kt @@ -35,55 +35,153 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ChronometerStateTest : SysuiTestCase() { - private lateinit var mockTimeSource: MutableTimeSource + private lateinit var fakeTimeSource: MutableTimeSource @Before fun setup() { - mockTimeSource = MutableTimeSource() + fakeTimeSource = MutableTimeSource() } @Test - fun initialText_isCorrect() = runTest { - val state = ChronometerState(mockTimeSource, 0L) - assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(0)) + fun initialText_isEventInFutureFalse_timeIsNow() = runTest { + fakeTimeSource.time = 3_000 + val state = + ChronometerState(fakeTimeSource, eventTimeMillis = 3_000, isEventInFuture = false) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 0)) } @Test - fun textUpdates_withTime() = runTest { - val startTime = 1000L - val state = ChronometerState(mockTimeSource, startTime) + fun initialText_isEventInFutureFalse_timeInPast() = runTest { + fakeTimeSource.time = 3_000 + val state = + ChronometerState(fakeTimeSource, eventTimeMillis = 1_000, isEventInFuture = false) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 2)) + } + + @Test + fun initialText_isEventInFutureFalse_timeInFuture() = runTest { + fakeTimeSource.time = 3_000 + val state = + ChronometerState(fakeTimeSource, eventTimeMillis = 5_000, isEventInFuture = false) + // When isEventInFuture=false, eventTimeMillis needs to be in the past if we want text to + // show + assertThat(state.currentTimeText).isNull() + } + + @Test + fun initialText_isEventInFutureTrue_timeIsNow() = runTest { + fakeTimeSource.time = 3_000 + val state = + ChronometerState(fakeTimeSource, eventTimeMillis = 3_000, isEventInFuture = true) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 0)) + } + + @Test + fun initialText_isEventInFutureTrue_timeInFuture() = runTest { + fakeTimeSource.time = 3_000 + val state = + ChronometerState(fakeTimeSource, eventTimeMillis = 5_000, isEventInFuture = true) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 2)) + } + + @Test + fun initialText_isEventInFutureTrue_timeInPast() = runTest { + fakeTimeSource.time = 3_000 + val state = + ChronometerState(fakeTimeSource, eventTimeMillis = 1_000, isEventInFuture = true) + // When isEventInFuture=true, eventTimeMillis needs to be in the future if we want text to + // show + assertThat(state.currentTimeText).isNull() + } + + @Test + fun textUpdates_isEventInFutureFalse_timeInPast() = runTest { + val eventTime = 1000L + val state = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = false) val job = launch { state.run() } val elapsedTime = 5000L - mockTimeSource.time = startTime + elapsedTime + fakeTimeSource.time = eventTime + elapsedTime advanceTimeBy(elapsedTime) assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(elapsedTime / 1000)) + val additionalTime = 6000L + fakeTimeSource.time += additionalTime + advanceTimeBy(additionalTime) + assertThat(state.currentTimeText) + .isEqualTo(formatElapsedTime((elapsedTime + additionalTime) / 1000)) + job.cancelAndJoin() } @Test - fun textUpdates_toLargerValue() = runTest { - val startTime = 1000L - val state = ChronometerState(mockTimeSource, startTime) + fun textUpdates_isEventInFutureFalse_timeChangesFromFutureToPast() = runTest { + val eventTime = 15_000L + val state = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = false) val job = launch { state.run() } - val elapsedTime = 15000L - mockTimeSource.time = startTime + elapsedTime - advanceTimeBy(elapsedTime) - assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(elapsedTime / 1000)) + // WHEN the time is 5 but the eventTime is 15 + fakeTimeSource.time = 5_000L + advanceTimeBy(5_000L) + // THEN no text is shown + assertThat(state.currentTimeText).isNull() + + // WHEN the time advances to 40 + fakeTimeSource.time = 40_000L + advanceTimeBy(35_000) + // THEN text is shown as 25 seconds (40 - 15) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 25)) job.cancelAndJoin() } @Test - fun textUpdates_afterResettingBase() = runTest { + fun textUpdates_isEventInFutureTrue_timeInFuture() = runTest { + val eventTime = 15_000L + val state = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = true) + val job = launch { state.run() } + + fakeTimeSource.time = 5_000L + advanceTimeBy(5_000L) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 10)) + + val additionalTime = 6000L + fakeTimeSource.time += additionalTime + advanceTimeBy(additionalTime) + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 4)) + + job.cancelAndJoin() + } + + @Test + fun textUpdates_isEventInFutureTrue_timeChangesFromFutureToPast() = runTest { + val eventTime = 15_000L + val state = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = true) + val job = launch { state.run() } + + // WHEN the time is 5 and the eventTime is 15 + fakeTimeSource.time = 5_000L + advanceTimeBy(5_000L) + // THEN 10 seconds is shown + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 10)) + + // WHEN the time advances to 40 (past the event time) + fakeTimeSource.time = 40_000L + advanceTimeBy(35_000) + // THEN no text is shown + assertThat(state.currentTimeText).isNull() + + job.cancelAndJoin() + } + + @Test + fun textUpdates_afterResettingBase_isEventInFutureFalse() = runTest { val initialElapsedTime = 30000L val startTime = 50000L - val state = ChronometerState(mockTimeSource, startTime) + val state = ChronometerState(fakeTimeSource, startTime, isEventInFuture = false) val job = launch { state.run() } - mockTimeSource.time = startTime + initialElapsedTime + fakeTimeSource.time = startTime + initialElapsedTime advanceTimeBy(initialElapsedTime) assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(initialElapsedTime / 1000)) @@ -91,15 +189,68 @@ class ChronometerStateTest : SysuiTestCase() { val newElapsedTime = 5000L val newStartTime = 100000L - val newState = ChronometerState(mockTimeSource, newStartTime) + val newState = ChronometerState(fakeTimeSource, newStartTime, isEventInFuture = false) val newJob = launch { newState.run() } - mockTimeSource.time = newStartTime + newElapsedTime + fakeTimeSource.time = newStartTime + newElapsedTime advanceTimeBy(newElapsedTime) assertThat(newState.currentTimeText).isEqualTo(formatElapsedTime(newElapsedTime / 1000)) newJob.cancelAndJoin() } + + @Test + fun textUpdates_afterResettingBase_isEventInFutureTrue() = runTest { + val initialElapsedTime = 40_000L + val eventTime = 50_000L + val state = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = true) + val job = launch { state.run() } + + fakeTimeSource.time = initialElapsedTime + advanceTimeBy(initialElapsedTime) + // Time should be 50 - 40 = 10 + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 10)) + + job.cancelAndJoin() + + val newElapsedTime = 75_000L + val newEventTime = 100_000L + val newState = ChronometerState(fakeTimeSource, newEventTime, isEventInFuture = true) + val newJob = launch { newState.run() } + + fakeTimeSource.time = newElapsedTime + advanceTimeBy(newElapsedTime - initialElapsedTime) + // Time should be 100 - 75 = 25 + assertThat(newState.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 25)) + + newJob.cancelAndJoin() + } + + @Test + fun textUpdates_afterResettingisEventInFuture() = runTest { + val initialElapsedTime = 40_000L + val eventTime = 50_000L + val state = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = true) + val job = launch { state.run() } + + fakeTimeSource.time = initialElapsedTime + advanceTimeBy(initialElapsedTime) + // Time should be 50 - 40 = 10 + assertThat(state.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 10)) + + job.cancelAndJoin() + + val newElapsedTime = 70_000L + val newState = ChronometerState(fakeTimeSource, eventTime, isEventInFuture = false) + val newJob = launch { newState.run() } + + fakeTimeSource.time = newElapsedTime + advanceTimeBy(newElapsedTime - initialElapsedTime) + // Time should be 70 - 50 = 20 + assertThat(newState.currentTimeText).isEqualTo(formatElapsedTime(/* elapsedSeconds= */ 20)) + + newJob.cancelAndJoin() + } } /** A fake implementation of [TimeSource] that allows the caller to set the current time */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 2fe627020ebf..06656b744a5a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -141,7 +141,7 @@ constructor( // When we're promoting notifications automatically, the `when` time set on the // notification will likely just be set to the current time, which would cause the chip // to always show "now". We don't want early testers to get that experience since it's - // not what will happen at launch, so just don't show any time. + // not what will happen at launch, so just don't show any time.onometerstate return OngoingActivityChipModel.Active.IconOnly( this.key, icon, @@ -194,12 +194,12 @@ constructor( } } is PromotedNotificationContentModel.When.Chronometer -> { - // TODO(b/364653005): Check isCountDown and support CountDown. return OngoingActivityChipModel.Active.Timer( this.key, icon, colors, startTimeMs = this.promotedContent.time.elapsedRealtimeMillis, + isEventInFuture = this.promotedContent.time.isCountDown, onClickListenerLegacy, clickBehavior, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/ChipChronometerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/ChipChronometerBinder.kt index 2032ec8af78c..1eb46d8cc3d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/ChipChronometerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/ChipChronometerBinder.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.ui.binder +import android.annotation.ElapsedRealtimeLong import com.android.systemui.statusbar.chips.ui.view.ChipChronometer object ChipChronometerBinder { @@ -25,9 +26,11 @@ object ChipChronometerBinder { * @param startTimeMs the time this event started, relative to * [com.android.systemui.util.time.SystemClock.elapsedRealtime]. See * [android.widget.Chronometer.setBase]. + * @param isCountDown see [android.widget.Chronometer.setCountDown]. */ - fun bind(startTimeMs: Long, view: ChipChronometer) { + fun bind(@ElapsedRealtimeLong startTimeMs: Long, isCountDown: Boolean, view: ChipChronometer) { view.base = startTimeMs + view.isCountDown = isCountDown view.start() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 6f8552738d33..77e0dde3dec5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -315,7 +315,11 @@ object OngoingActivityChipBinder { chipShortTimeDeltaView.visibility = View.GONE } is OngoingActivityChipModel.Active.Timer -> { - ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView) + ChipChronometerBinder.bind( + chipModel.startTimeMs, + chipModel.isEventInFuture, + chipTimeView, + ) chipTimeView.visibility = View.VISIBLE chipTextView.visibility = View.GONE diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index 55d753662a65..f650d8d7d10d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -74,25 +74,30 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = val textMeasurer = rememberTextMeasurer() when (viewModel) { is OngoingActivityChipModel.Active.Timer -> { - val timerState = rememberChronometerState(startTimeMillis = viewModel.startTimeMs) - val text = timerState.currentTimeText - Text( - text = text, - style = textStyle, - color = textColor, - softWrap = false, - modifier = - modifier - .hideTextIfDoesNotFit( - text = text, - textStyle = textStyle, - textMeasurer = textMeasurer, - maxTextWidth = maxTextWidth, - startPadding = startPadding, - endPadding = endPadding, - ) - .neverDecreaseWidth(density), - ) + val timerState = + rememberChronometerState( + eventTimeMillis = viewModel.startTimeMs, + isCountDown = viewModel.isEventInFuture, + ) + timerState.currentTimeText?.let { text -> + Text( + text = text, + style = textStyle, + color = textColor, + softWrap = false, + modifier = + modifier + .hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ) + .neverDecreaseWidth(density), + ) + } } is OngoingActivityChipModel.Active.Countdown -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 8e470742f174..106a36313190 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -105,6 +105,12 @@ sealed class OngoingActivityChipModel { * [android.widget.Chronometer.setBase]. */ @ElapsedRealtimeLong val startTimeMs: Long, + /** + * True if this chip represents an event starting in the future and false if this chip + * represents an event that has already started. If true, [startTimeMs] should be in the + * future. Otherwise, [startTimeMs] should be in the past. + */ + val isEventInFuture: Boolean = false, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val isHidden: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerState.kt index 62789782d0a9..7402583cafe2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChronometerState.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.statusbar.chips.ui.viewmodel +import android.annotation.ElapsedRealtimeLong import android.os.SystemClock import android.text.format.DateUtils.formatElapsedTime import androidx.compose.runtime.Composable @@ -27,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle +import kotlin.math.absoluteValue import kotlinx.coroutines.delay /** Platform-optimized interface for getting current time */ @@ -34,18 +36,50 @@ fun interface TimeSource { fun getCurrentTime(): Long } -/** Holds and manages the state for a Chronometer */ -class ChronometerState(private val timeSource: TimeSource, private val startTimeMillis: Long) { - private var currentTimeMillis by mutableLongStateOf(0L) +/** + * Holds and manages the state for a Chronometer, which shows a timer in a format like "MM:SS" or + * "H:MM:SS". + * + * If [isEventInFuture] is false, then this Chronometer is counting up from an event that started in + * the past, like a phone call that was answered. [eventTimeMillis] represents the time the event + * started and the timer will tick up: 04:00, 04:01, ... No timer is shown if [eventTimeMillis] is + * in the future and [isEventInFuture] is false. + * + * If [isEventInFuture] is true, then this Chronometer is counting down to an event that will occur + * in the future, like a future meeting. [eventTimeMillis] represents the time the event will occur + * and the timer will tick down: 04:00, 03:59, ... No timer is shown if [eventTimeMillis] is in the + * past and [isEventInFuture] is true. + */ +class ChronometerState( + private val timeSource: TimeSource, + @ElapsedRealtimeLong private val eventTimeMillis: Long, + private val isEventInFuture: Boolean, +) { + private var currentTimeMillis by mutableLongStateOf(timeSource.getCurrentTime()) private val elapsedTimeMillis: Long - get() = maxOf(0L, currentTimeMillis - startTimeMillis) + get() = + if (isEventInFuture) { + eventTimeMillis - currentTimeMillis + } else { + currentTimeMillis - eventTimeMillis + } - val currentTimeText: String by derivedStateOf { formatElapsedTime(elapsedTimeMillis / 1000) } + /** + * The current timer string in a format like "MM:SS" or "H:MM:SS", or null if we shouldn't show + * the timer string. + */ + val currentTimeText: String? by derivedStateOf { + if (elapsedTimeMillis < 0) { + null + } else { + formatElapsedTime(elapsedTimeMillis / 1000) + } + } suspend fun run() { while (true) { currentTimeMillis = timeSource.getCurrentTime() - val delaySkewMillis = (currentTimeMillis - startTimeMillis) % 1000L + val delaySkewMillis = (eventTimeMillis - currentTimeMillis).absoluteValue % 1000L delay(1000L - delaySkewMillis) } } @@ -54,13 +88,16 @@ class ChronometerState(private val timeSource: TimeSource, private val startTime /** Remember and manage the ChronometerState */ @Composable fun rememberChronometerState( - startTimeMillis: Long, + eventTimeMillis: Long, + isCountDown: Boolean, timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } }, ): ChronometerState { val state = - remember(timeSource, startTimeMillis) { ChronometerState(timeSource, startTimeMillis) } + remember(timeSource, eventTimeMillis, isCountDown) { + ChronometerState(timeSource, eventTimeMillis, isCountDown) + } val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(lifecycleOwner, timeSource, startTimeMillis) { + LaunchedEffect(lifecycleOwner, timeSource, eventTimeMillis) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { state.run() } } |