Skip to content

Commit afe133e

Browse files
authored
Merge pull request #8127 from vector-im/bugfix/fre/fix_countuptimer_stackoverflow
Fix StackOverFlow exception when stop action is called within the tick event
2 parents 8667797 + 225f51d commit afe133e

File tree

3 files changed

+94
-9
lines changed

3 files changed

+94
-9
lines changed

changelog.d/8127.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CountUpTimer - Fix StackOverFlow exception when stop action is called within the tick event

library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class CountUpTimer(
3535
private val elapsedTime: AtomicLong = AtomicLong(0)
3636

3737
private fun startCounter() {
38+
counterJob?.cancel()
3839
counterJob = coroutineScope.launch {
3940
while (true) {
4041
delay(intervalInMs - elapsedTime() % intervalInMs)
@@ -54,29 +55,54 @@ class CountUpTimer(
5455
}
5556
}
5657

58+
/**
59+
* Start a new timer with the initial given time, if any.
60+
* If the timer is already started, it will be restarted.
61+
*/
5762
fun start(initialTime: Long = 0L) {
5863
elapsedTime.set(initialTime)
59-
resume()
64+
lastTime.set(clock.epochMillis())
65+
startCounter()
6066
}
6167

68+
/**
69+
* Pause the timer at the current time.
70+
*/
6271
fun pause() {
63-
tickListener?.onTick(elapsedTime())
64-
counterJob?.cancel()
65-
counterJob = null
72+
pauseAndTick()
6673
}
6774

75+
/**
76+
* Resume the timer from the current time.
77+
* Does nothing if the timer is already running.
78+
*/
6879
fun resume() {
69-
lastTime.set(clock.epochMillis())
70-
startCounter()
80+
if (counterJob?.isActive != true) {
81+
lastTime.set(clock.epochMillis())
82+
startCounter()
83+
}
7184
}
7285

86+
/**
87+
* Stop and reset the timer.
88+
*/
7389
fun stop() {
74-
tickListener?.onTick(elapsedTime())
75-
counterJob?.cancel()
76-
counterJob = null
90+
pauseAndTick()
7791
elapsedTime.set(0L)
7892
}
7993

94+
private fun pauseAndTick() {
95+
if (counterJob?.isActive == true) {
96+
// get the elapsed time before cancelling the timer
97+
val elapsedTime = elapsedTime()
98+
// cancel the timer before ticking
99+
counterJob?.cancel()
100+
counterJob = null
101+
// tick with the computed elapsed time
102+
tickListener?.onTick(elapsedTime)
103+
}
104+
}
105+
80106
fun interface TickListener {
81107
fun onTick(milliseconds: Long)
82108
}

library/core-utils/src/test/java/im/vector/lib/core/utils/timer/CountUpTimerTest.kt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package im.vector.lib.core.utils.timer
1919
import im.vector.lib.core.utils.test.fakes.FakeClock
2020
import io.mockk.every
2121
import io.mockk.mockk
22+
import io.mockk.spyk
23+
import io.mockk.verify
2224
import io.mockk.verifySequence
2325
import kotlinx.coroutines.ExperimentalCoroutinesApi
2426
import kotlinx.coroutines.test.advanceTimeBy
@@ -36,6 +38,7 @@ internal class CountUpTimerTest {
3638

3739
@Test
3840
fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest {
41+
// Given
3942
every { fakeClock.epochMillis() } answers { currentTime }
4043
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
4144
val timer = CountUpTimer(
@@ -44,6 +47,7 @@ internal class CountUpTimerTest {
4447
intervalInMs = AN_INTERVAL,
4548
).also { it.tickListener = tickListener }
4649

50+
// When
4751
timer.start()
4852
advanceTimeBy(AN_INTERVAL / 2) // no tick
4953
timer.pause() // tick
@@ -52,6 +56,7 @@ internal class CountUpTimerTest {
5256
advanceTimeBy(AN_INTERVAL * 4) // tick * 4
5357
timer.stop() // tick
5458

59+
// Then
5560
verifySequence {
5661
tickListener.onTick(AN_INTERVAL / 2)
5762
tickListener.onTick(AN_INTERVAL)
@@ -64,6 +69,7 @@ internal class CountUpTimerTest {
6469

6570
@Test
6671
fun `given an initial time, the timer ticks the right values at the right moments`() = runTest {
72+
// Given
6773
every { fakeClock.epochMillis() } answers { currentTime }
6874
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
6975
val timer = CountUpTimer(
@@ -72,6 +78,7 @@ internal class CountUpTimerTest {
7278
intervalInMs = AN_INTERVAL,
7379
).also { it.tickListener = tickListener }
7480

81+
// When
7582
timer.start(AN_INITIAL_TIME)
7683
advanceTimeBy(AN_INTERVAL) // tick
7784
timer.pause() // tick
@@ -80,6 +87,7 @@ internal class CountUpTimerTest {
8087
advanceTimeBy(AN_INTERVAL * 4) // tick * 4
8188
timer.stop() // tick
8289

90+
// Then
8391
val offset = AN_INITIAL_TIME % AN_INTERVAL
8492
verifySequence {
8593
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset)
@@ -91,4 +99,54 @@ internal class CountUpTimerTest {
9199
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5)
92100
}
93101
}
102+
103+
@Test
104+
fun `when stopping the timer on tick, the stop action is called twice and the timer ticks twice`() = runTest {
105+
// Given
106+
every { fakeClock.epochMillis() } answers { currentTime }
107+
val timer = spyk(
108+
CountUpTimer(
109+
coroutineScope = this,
110+
clock = fakeClock,
111+
intervalInMs = AN_INTERVAL,
112+
)
113+
)
114+
val tickListener = mockk<CountUpTimer.TickListener> {
115+
every { onTick(any()) } answers { timer.stop() }
116+
}
117+
timer.tickListener = tickListener
118+
119+
// When
120+
timer.start()
121+
advanceTimeBy(AN_INTERVAL * 10)
122+
123+
// Then
124+
verify(exactly = 2) { timer.stop() } // one call at the first tick, a second time because of the tick of the first stop
125+
verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the stop action
126+
}
127+
128+
@Test
129+
fun `when pausing the timer on tick, the pause action is called twice and the timer ticks twice`() = runTest {
130+
// Given
131+
every { fakeClock.epochMillis() } answers { currentTime }
132+
val timer = spyk(
133+
CountUpTimer(
134+
coroutineScope = this,
135+
clock = fakeClock,
136+
intervalInMs = AN_INTERVAL,
137+
)
138+
)
139+
val tickListener = mockk<CountUpTimer.TickListener> {
140+
every { onTick(any()) } answers { timer.pause() }
141+
}
142+
timer.tickListener = tickListener
143+
144+
// When
145+
timer.start()
146+
advanceTimeBy(AN_INTERVAL * 10)
147+
148+
// Then
149+
verify(exactly = 2) { timer.pause() } // one call at the first tick, a second time because of the tick of the first pause
150+
verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the pause action
151+
}
94152
}

0 commit comments

Comments
 (0)