Prevent crashes in AndroidFlingSpline#flingPosition Clamps time position received when calculating the resulting fling position to prevent IndexOutOfBoundsException when the play time happens to be lower than the start time. This is a workaround, as the playTime is otherwise expected to be non-decreasing for the fling calculation. Bug: 313685022 Test: AndroidFlingSplineTest#testClampedOutOfRangePosition Change-Id: Ib1e20d87f1cb1e89aea8194e24841a05df4b280b
diff --git a/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/AndroidFlingSplineTest.kt b/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/AndroidFlingSplineTest.kt new file mode 100644 index 0000000..45c7383 --- /dev/null +++ b/compose/animation/animation/src/androidUnitTest/kotlin/androidx/compose/animation/AndroidFlingSplineTest.kt
@@ -0,0 +1,39 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 androidx.compose.animation + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class AndroidFlingSplineTest { + + @Test + fun testClampedOutOfRangePosition() { + val controlStart = AndroidFlingSpline.flingPosition(0f) + val controlEnd = AndroidFlingSpline.flingPosition(1f) + + assertEquals(controlStart, AndroidFlingSpline.flingPosition(-10f)) + assertEquals(controlStart, AndroidFlingSpline.flingPosition(-1f)) + assertEquals(controlStart, AndroidFlingSpline.flingPosition(-0.06f)) + + assertEquals(controlEnd, AndroidFlingSpline.flingPosition(1.5f)) + assertEquals(controlEnd, AndroidFlingSpline.flingPosition(10f)) + } +}
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt index 04e8097..8c6e47f 100644 --- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt +++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SplineBasedDecay.kt
@@ -87,7 +87,10 @@ * @param time progress through the fling animation from 0-1 */ fun flingPosition(time: Float): FlingResult { - val index = (NbSamples * time).toInt() + // We clamp the time to prevent crashes from a clock providing playTime values lower than + // the start time, which leads to an IOO here. See b/313685022. + val clampedTime = time.coerceIn(0f, 1f) + val index = (NbSamples * clampedTime).toInt() var distanceCoef = 1f var velocityCoef = 0f if (index < NbSamples) { @@ -96,7 +99,7 @@ val dInf = SplinePositions[index] val dSup = SplinePositions[index + 1] velocityCoef = (dSup - dInf) / (tSup - tInf) - distanceCoef = dInf + (time - tInf) * velocityCoef + distanceCoef = dInf + (clampedTime - tInf) * velocityCoef } return FlingResult(distanceCoefficient = distanceCoef, velocityCoefficient = velocityCoef) }