Skip to content

Commit f74a885

Browse files
committed
Merge branch 'hotfix/1.5.25' into main
2 parents 8667797 + 4cf7879 commit f74a885

File tree

8 files changed

+188
-12
lines changed

8 files changed

+188
-12
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
Changes in Element v1.5.25 (2023-02-15)
2+
=======================================
3+
4+
Bugfixes 🐛
5+
----------
6+
- CountUpTimer - Fix StackOverFlow exception when stop action is called within the tick event ([#8127](https://github.com/vector-im/element-android/issues/8127))
7+
8+
19
Changes in Element v1.5.24 (2023-02-08)
210
=======================================
311

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Main changes in this version: Mainly bugfixing, in particular fix message not appearing on the timeline.
2+
Full changelog: https://github.com/vector-im/element-android/releases

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@ class CountUpTimer(
3333

3434
private val lastTime: AtomicLong = AtomicLong(clock.epochMillis())
3535
private val elapsedTime: AtomicLong = AtomicLong(0)
36+
// To ensure that the regular tick value is an exact multiple of `intervalInMs`
37+
private val specialRound = SpecialRound(intervalInMs)
3638

3739
private fun startCounter() {
40+
counterJob?.cancel()
3841
counterJob = coroutineScope.launch {
3942
while (true) {
4043
delay(intervalInMs - elapsedTime() % intervalInMs)
41-
tickListener?.onTick(elapsedTime())
44+
tickListener?.onTick(specialRound.round(elapsedTime()))
4245
}
4346
}
4447
}
@@ -54,29 +57,54 @@ class CountUpTimer(
5457
}
5558
}
5659

60+
/**
61+
* Start a new timer with the initial given time, if any.
62+
* If the timer is already started, it will be restarted.
63+
*/
5764
fun start(initialTime: Long = 0L) {
5865
elapsedTime.set(initialTime)
59-
resume()
66+
lastTime.set(clock.epochMillis())
67+
startCounter()
6068
}
6169

70+
/**
71+
* Pause the timer at the current time.
72+
*/
6273
fun pause() {
63-
tickListener?.onTick(elapsedTime())
64-
counterJob?.cancel()
65-
counterJob = null
74+
pauseAndTick()
6675
}
6776

77+
/**
78+
* Resume the timer from the current time.
79+
* Does nothing if the timer is already running.
80+
*/
6881
fun resume() {
69-
lastTime.set(clock.epochMillis())
70-
startCounter()
82+
if (counterJob?.isActive != true) {
83+
lastTime.set(clock.epochMillis())
84+
startCounter()
85+
}
7186
}
7287

88+
/**
89+
* Stop and reset the timer.
90+
*/
7391
fun stop() {
74-
tickListener?.onTick(elapsedTime())
75-
counterJob?.cancel()
76-
counterJob = null
92+
pauseAndTick()
7793
elapsedTime.set(0L)
7894
}
7995

96+
private fun pauseAndTick() {
97+
if (counterJob?.isActive == true) {
98+
// get the elapsed time before cancelling the timer
99+
val elapsedTime = elapsedTime()
100+
// cancel the timer before ticking
101+
counterJob?.cancel()
102+
counterJob = null
103+
// tick with the computed elapsed time
104+
tickListener?.onTick(elapsedTime)
105+
}
106+
}
107+
80108
fun interface TickListener {
81109
fun onTick(milliseconds: Long)
82110
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.lib.core.utils.timer
18+
19+
import kotlin.math.round
20+
21+
class SpecialRound(private val step: Long) {
22+
/**
23+
* Round the provided value to the nearest multiple of `step`.
24+
*/
25+
fun round(value: Long): Long {
26+
return round(value.toDouble() / step).toLong() * step
27+
}
28+
}

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
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.lib.core.utils.timer
18+
19+
import org.amshove.kluent.shouldBeEqualTo
20+
import org.junit.Test
21+
22+
class SpecialRoundTest {
23+
@Test
24+
fun `test special round 500`() {
25+
val sut = SpecialRound(500)
26+
sut.round(1) shouldBeEqualTo 0
27+
sut.round(499) shouldBeEqualTo 500
28+
sut.round(500) shouldBeEqualTo 500
29+
sut.round(501) shouldBeEqualTo 500
30+
sut.round(999) shouldBeEqualTo 1_000
31+
sut.round(1000) shouldBeEqualTo 1_000
32+
sut.round(1001) shouldBeEqualTo 1_000
33+
sut.round(1499) shouldBeEqualTo 1_500
34+
sut.round(1500) shouldBeEqualTo 1_500
35+
sut.round(1501) shouldBeEqualTo 1_500
36+
}
37+
38+
@Test
39+
fun `test special round 1_000`() {
40+
val sut = SpecialRound(1_000)
41+
sut.round(1) shouldBeEqualTo 0
42+
sut.round(499) shouldBeEqualTo 0
43+
sut.round(500) shouldBeEqualTo 0
44+
sut.round(501) shouldBeEqualTo 1_000
45+
sut.round(999) shouldBeEqualTo 1_000
46+
sut.round(1000) shouldBeEqualTo 1_000
47+
sut.round(1001) shouldBeEqualTo 1_000
48+
sut.round(1499) shouldBeEqualTo 1_000
49+
sut.round(1500) shouldBeEqualTo 2_000
50+
sut.round(1501) shouldBeEqualTo 2_000
51+
}
52+
}

matrix-sdk-android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ android {
6262
// that the app's state is completely cleared between tests.
6363
testInstrumentationRunnerArguments clearPackageData: 'true'
6464

65-
buildConfigField "String", "SDK_VERSION", "\"1.5.24\""
65+
buildConfigField "String", "SDK_VERSION", "\"1.5.25\""
6666

6767
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
6868
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

vector-app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ ext.versionMinor = 5
3737
// Note: even values are reserved for regular release, odd values for hotfix release.
3838
// When creating a hotfix, you should decrease the value, since the current value
3939
// is the value for the next regular release.
40-
ext.versionPatch = 24
40+
ext.versionPatch = 25
4141

4242
static def getGitTimestamp() {
4343
def cmd = 'git show -s --format=%ct'

0 commit comments

Comments
 (0)