Merge tag 'v1.5.25' into sc
Change-Id: I45d2198b25e7ebee157f4836596ef59ddb1fbe8e
This commit is contained in:
commit
daacdd038c
|
@ -1,3 +1,11 @@
|
||||||
|
Changes in Element v1.5.25 (2023-02-15)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- CountUpTimer - Fix StackOverFlow exception when stop action is called within the tick event ([#8127](https://github.com/vector-im/element-android/issues/8127))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.5.24 (2023-02-08)
|
Changes in Element v1.5.24 (2023-02-08)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Main changes in this version: Mainly bugfixing, in particular fix message not appearing on the timeline.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases
|
|
@ -33,12 +33,15 @@ class CountUpTimer(
|
||||||
|
|
||||||
private val lastTime: AtomicLong = AtomicLong(clock.epochMillis())
|
private val lastTime: AtomicLong = AtomicLong(clock.epochMillis())
|
||||||
private val elapsedTime: AtomicLong = AtomicLong(0)
|
private val elapsedTime: AtomicLong = AtomicLong(0)
|
||||||
|
// To ensure that the regular tick value is an exact multiple of `intervalInMs`
|
||||||
|
private val specialRound = SpecialRound(intervalInMs)
|
||||||
|
|
||||||
private fun startCounter() {
|
private fun startCounter() {
|
||||||
|
counterJob?.cancel()
|
||||||
counterJob = coroutineScope.launch {
|
counterJob = coroutineScope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(intervalInMs - elapsedTime() % intervalInMs)
|
delay(intervalInMs - elapsedTime() % intervalInMs)
|
||||||
tickListener?.onTick(elapsedTime())
|
tickListener?.onTick(specialRound.round(elapsedTime()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,29 +57,54 @@ class CountUpTimer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new timer with the initial given time, if any.
|
||||||
|
* If the timer is already started, it will be restarted.
|
||||||
|
*/
|
||||||
fun start(initialTime: Long = 0L) {
|
fun start(initialTime: Long = 0L) {
|
||||||
elapsedTime.set(initialTime)
|
elapsedTime.set(initialTime)
|
||||||
resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pause() {
|
|
||||||
tickListener?.onTick(elapsedTime())
|
|
||||||
counterJob?.cancel()
|
|
||||||
counterJob = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resume() {
|
|
||||||
lastTime.set(clock.epochMillis())
|
lastTime.set(clock.epochMillis())
|
||||||
startCounter()
|
startCounter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the timer at the current time.
|
||||||
|
*/
|
||||||
|
fun pause() {
|
||||||
|
pauseAndTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the timer from the current time.
|
||||||
|
* Does nothing if the timer is already running.
|
||||||
|
*/
|
||||||
|
fun resume() {
|
||||||
|
if (counterJob?.isActive != true) {
|
||||||
|
lastTime.set(clock.epochMillis())
|
||||||
|
startCounter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop and reset the timer.
|
||||||
|
*/
|
||||||
fun stop() {
|
fun stop() {
|
||||||
tickListener?.onTick(elapsedTime())
|
pauseAndTick()
|
||||||
counterJob?.cancel()
|
|
||||||
counterJob = null
|
|
||||||
elapsedTime.set(0L)
|
elapsedTime.set(0L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pauseAndTick() {
|
||||||
|
if (counterJob?.isActive == true) {
|
||||||
|
// get the elapsed time before cancelling the timer
|
||||||
|
val elapsedTime = elapsedTime()
|
||||||
|
// cancel the timer before ticking
|
||||||
|
counterJob?.cancel()
|
||||||
|
counterJob = null
|
||||||
|
// tick with the computed elapsed time
|
||||||
|
tickListener?.onTick(elapsedTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun interface TickListener {
|
fun interface TickListener {
|
||||||
fun onTick(milliseconds: Long)
|
fun onTick(milliseconds: Long)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.lib.core.utils.timer
|
||||||
|
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
class SpecialRound(private val step: Long) {
|
||||||
|
/**
|
||||||
|
* Round the provided value to the nearest multiple of `step`.
|
||||||
|
*/
|
||||||
|
fun round(value: Long): Long {
|
||||||
|
return round(value.toDouble() / step).toLong() * step
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,8 @@ package im.vector.lib.core.utils.timer
|
||||||
import im.vector.lib.core.utils.test.fakes.FakeClock
|
import im.vector.lib.core.utils.test.fakes.FakeClock
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
import io.mockk.verifySequence
|
import io.mockk.verifySequence
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
@ -36,6 +38,7 @@ internal class CountUpTimerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest {
|
fun `when pausing and resuming the timer, the timer ticks the right values at the right moments`() = runTest {
|
||||||
|
// Given
|
||||||
every { fakeClock.epochMillis() } answers { currentTime }
|
every { fakeClock.epochMillis() } answers { currentTime }
|
||||||
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
|
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
|
||||||
val timer = CountUpTimer(
|
val timer = CountUpTimer(
|
||||||
|
@ -44,6 +47,7 @@ internal class CountUpTimerTest {
|
||||||
intervalInMs = AN_INTERVAL,
|
intervalInMs = AN_INTERVAL,
|
||||||
).also { it.tickListener = tickListener }
|
).also { it.tickListener = tickListener }
|
||||||
|
|
||||||
|
// When
|
||||||
timer.start()
|
timer.start()
|
||||||
advanceTimeBy(AN_INTERVAL / 2) // no tick
|
advanceTimeBy(AN_INTERVAL / 2) // no tick
|
||||||
timer.pause() // tick
|
timer.pause() // tick
|
||||||
|
@ -52,6 +56,7 @@ internal class CountUpTimerTest {
|
||||||
advanceTimeBy(AN_INTERVAL * 4) // tick * 4
|
advanceTimeBy(AN_INTERVAL * 4) // tick * 4
|
||||||
timer.stop() // tick
|
timer.stop() // tick
|
||||||
|
|
||||||
|
// Then
|
||||||
verifySequence {
|
verifySequence {
|
||||||
tickListener.onTick(AN_INTERVAL / 2)
|
tickListener.onTick(AN_INTERVAL / 2)
|
||||||
tickListener.onTick(AN_INTERVAL)
|
tickListener.onTick(AN_INTERVAL)
|
||||||
|
@ -64,6 +69,7 @@ internal class CountUpTimerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given an initial time, the timer ticks the right values at the right moments`() = runTest {
|
fun `given an initial time, the timer ticks the right values at the right moments`() = runTest {
|
||||||
|
// Given
|
||||||
every { fakeClock.epochMillis() } answers { currentTime }
|
every { fakeClock.epochMillis() } answers { currentTime }
|
||||||
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
|
val tickListener = mockk<CountUpTimer.TickListener>(relaxed = true)
|
||||||
val timer = CountUpTimer(
|
val timer = CountUpTimer(
|
||||||
|
@ -72,6 +78,7 @@ internal class CountUpTimerTest {
|
||||||
intervalInMs = AN_INTERVAL,
|
intervalInMs = AN_INTERVAL,
|
||||||
).also { it.tickListener = tickListener }
|
).also { it.tickListener = tickListener }
|
||||||
|
|
||||||
|
// When
|
||||||
timer.start(AN_INITIAL_TIME)
|
timer.start(AN_INITIAL_TIME)
|
||||||
advanceTimeBy(AN_INTERVAL) // tick
|
advanceTimeBy(AN_INTERVAL) // tick
|
||||||
timer.pause() // tick
|
timer.pause() // tick
|
||||||
|
@ -80,6 +87,7 @@ internal class CountUpTimerTest {
|
||||||
advanceTimeBy(AN_INTERVAL * 4) // tick * 4
|
advanceTimeBy(AN_INTERVAL * 4) // tick * 4
|
||||||
timer.stop() // tick
|
timer.stop() // tick
|
||||||
|
|
||||||
|
// Then
|
||||||
val offset = AN_INITIAL_TIME % AN_INTERVAL
|
val offset = AN_INITIAL_TIME % AN_INTERVAL
|
||||||
verifySequence {
|
verifySequence {
|
||||||
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset)
|
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL - offset)
|
||||||
|
@ -91,4 +99,54 @@ internal class CountUpTimerTest {
|
||||||
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5)
|
tickListener.onTick(AN_INITIAL_TIME + AN_INTERVAL * 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when stopping the timer on tick, the stop action is called twice and the timer ticks twice`() = runTest {
|
||||||
|
// Given
|
||||||
|
every { fakeClock.epochMillis() } answers { currentTime }
|
||||||
|
val timer = spyk(
|
||||||
|
CountUpTimer(
|
||||||
|
coroutineScope = this,
|
||||||
|
clock = fakeClock,
|
||||||
|
intervalInMs = AN_INTERVAL,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val tickListener = mockk<CountUpTimer.TickListener> {
|
||||||
|
every { onTick(any()) } answers { timer.stop() }
|
||||||
|
}
|
||||||
|
timer.tickListener = tickListener
|
||||||
|
|
||||||
|
// When
|
||||||
|
timer.start()
|
||||||
|
advanceTimeBy(AN_INTERVAL * 10)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify(exactly = 2) { timer.stop() } // one call at the first tick, a second time because of the tick of the first stop
|
||||||
|
verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the stop action
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when pausing the timer on tick, the pause action is called twice and the timer ticks twice`() = runTest {
|
||||||
|
// Given
|
||||||
|
every { fakeClock.epochMillis() } answers { currentTime }
|
||||||
|
val timer = spyk(
|
||||||
|
CountUpTimer(
|
||||||
|
coroutineScope = this,
|
||||||
|
clock = fakeClock,
|
||||||
|
intervalInMs = AN_INTERVAL,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val tickListener = mockk<CountUpTimer.TickListener> {
|
||||||
|
every { onTick(any()) } answers { timer.pause() }
|
||||||
|
}
|
||||||
|
timer.tickListener = tickListener
|
||||||
|
|
||||||
|
// When
|
||||||
|
timer.start()
|
||||||
|
advanceTimeBy(AN_INTERVAL * 10)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify(exactly = 2) { timer.pause() } // one call at the first tick, a second time because of the tick of the first pause
|
||||||
|
verify(exactly = 2) { tickListener.onTick(any()) } // one after reaching the first interval, a second after the pause action
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.lib.core.utils.timer
|
||||||
|
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SpecialRoundTest {
|
||||||
|
@Test
|
||||||
|
fun `test special round 500`() {
|
||||||
|
val sut = SpecialRound(500)
|
||||||
|
sut.round(1) shouldBeEqualTo 0
|
||||||
|
sut.round(499) shouldBeEqualTo 500
|
||||||
|
sut.round(500) shouldBeEqualTo 500
|
||||||
|
sut.round(501) shouldBeEqualTo 500
|
||||||
|
sut.round(999) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1000) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1001) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1499) shouldBeEqualTo 1_500
|
||||||
|
sut.round(1500) shouldBeEqualTo 1_500
|
||||||
|
sut.round(1501) shouldBeEqualTo 1_500
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test special round 1_000`() {
|
||||||
|
val sut = SpecialRound(1_000)
|
||||||
|
sut.round(1) shouldBeEqualTo 0
|
||||||
|
sut.round(499) shouldBeEqualTo 0
|
||||||
|
sut.round(500) shouldBeEqualTo 0
|
||||||
|
sut.round(501) shouldBeEqualTo 1_000
|
||||||
|
sut.round(999) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1000) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1001) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1499) shouldBeEqualTo 1_000
|
||||||
|
sut.round(1500) shouldBeEqualTo 2_000
|
||||||
|
sut.round(1501) shouldBeEqualTo 2_000
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,7 +62,7 @@ android {
|
||||||
// that the app's state is completely cleared between tests.
|
// that the app's state is completely cleared between tests.
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "SDK_VERSION", "\"1.5.24\""
|
buildConfigField "String", "SDK_VERSION", "\"1.5.25\""
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
|
||||||
|
|
|
@ -37,7 +37,7 @@ ext.versionMinor = 5
|
||||||
// Note: even values are reserved for regular release, odd values for hotfix release.
|
// Note: even values are reserved for regular release, odd values for hotfix release.
|
||||||
// When creating a hotfix, you should decrease the value, since the current value
|
// When creating a hotfix, you should decrease the value, since the current value
|
||||||
// is the value for the next regular release.
|
// is the value for the next regular release.
|
||||||
ext.versionPatch = 24
|
ext.versionPatch = 25
|
||||||
|
|
||||||
ext.scVersion = 62
|
ext.scVersion = 62
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue