saket / telephoto

Building blocks for designing media experiences in Compose UI
https://saket.github.io/telephoto/
Apache License 2.0
996 stars 32 forks source link

IllegalStateException: maximumVelocity should be a positive value #81

Open SimonMarquis opened 7 months ago

SimonMarquis commented 7 months ago

We are seeing a pretty substantial amount of errors in the latest version (0.9.0) that leads to this method:

https://github.com/saket/telephoto/blob/3e0c5573151e64908a024425bda9cd0be19f4d68/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/transformable.kt#L152

And all the stacktraces look like this (with value 0.0):

Fatal Exception: java.lang.IllegalStateException
maximumVelocity should be a positive value. You specified=0.0
androidx.compose.ui.input.pointer.util.VelocityTracker1D.calculateVelocity (VelocityTracker.kt:283)
androidx.compose.ui.input.pointer.util.VelocityTracker.calculateVelocity-AH228Gc (VelocityTracker.kt:102)
me.saket.telephoto.zoomable.internal.TransformableNode$pointerInputNode$1$1$2.invokeSuspend (transformable.kt:152)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTaskKt.resume (DispatchedTask.kt:175)
kotlinx.coroutines.DispatchedTaskKt.dispatch (DispatchedTask.kt:164)

or this (with a tiny value, that looks like a 0 approximation in float):

Fatal Exception: java.lang.IllegalStateException
maximumVelocity should be a positive value. You specified=-1.3029473E-34
androidx.compose.ui.input.pointer.util.VelocityTracker1D.calculateVelocity (VelocityTracker.kt:283)
androidx.compose.ui.input.pointer.util.VelocityTracker.calculateVelocity-AH228Gc (VelocityTracker.kt:102)
me.saket.telephoto.zoomable.internal.TransformableNode$pointerInputNode$1$1$2.invokeSuspend (transformable.kt:152)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTaskKt.resume (DispatchedTask.kt:175)
kotlinx.coroutines.DispatchedTaskKt.dispatch (DispatchedTask.kt:164)

I'm not sure why it would throw here to be honest, since we always provide sane values Velocity(Int.MAX_VALUE.toFloat(), Int.MAX_VALUE.toFloat()). Could this be an issue with the devices packing floats incorrectly?

The device repartition is as follow:

saket commented 7 months ago

Just to confirm, were you not seeing any of these crashes on an older version of telephoto?

saket commented 7 months ago

Can you also share your version of Compose UI?

SimonMarquis commented 7 months ago

Oh you're right, we did have them on previous versions as well (Crashlytics groupped them differently):

androidx.compose.ui.input.pointer.util.VelocityTracker1D.calculateVelocity (VelocityTracker.kt:283)
androidx.compose.ui.input.pointer.util.VelocityTracker.calculateVelocity-AH228Gc (VelocityTracker.kt:102)
androidx.compose.ui.input.pointer.util.VelocityTracker.calculateVelocity-9UxMQ8M (VelocityTracker.kt:82)
me.saket.telephoto.zoomable.internal.TransformableKt.calculateVelocity-OGnQdUo (transformable.kt:267)
me.saket.telephoto.zoomable.internal.TransformableKt.access$calculateVelocity-OGnQdUo (transformable.kt:1)
me.saket.telephoto.zoomable.internal.TransformableNode$pointerInputNode$1$1$2.invokeSuspend (transformable.kt:156)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTaskKt.resume (DispatchedTask.kt:175)
kotlinx.coroutines.DispatchedTaskKt.dispatch (DispatchedTask.kt:164)

Our Compose UI version is currently at 1.6.4.

saket commented 7 months ago

I've replaced my manual calculation of maximum velocity with the framework one in https://github.com/saket/telephoto/commit/32309870a59a14d84be87b1a6cc39dc6be79b781. Let's see if this fixes the issue.

https://github.com/saket/telephoto/blob/71b96177f293dcb27b64a456894871534b31ea8b/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/transformable.kt#L154-L156

I'm not certain, but https://github.com/saket/telephoto/pull/76 could also help.

I'll cut a release soon. In the meantime, the changes are available in 0.10.0-SNAPSHOT if your project is comfortable using snapshots.

SimonMarquis commented 7 months ago

Thanks for these updates. Unfortunately I won't be able to use such SNAPSHOT version in the main project for now 😖 I'll try to see if we can fork and test this patch.

saket commented 7 months ago

I just released https://github.com/saket/telephoto/releases/tag/0.10.0. Can you please give it a try?

SimonMarquis commented 7 months ago

Our app update will be rolled-out starting next monday 🚀

saket commented 6 months ago

@SimonMarquis were you able to verify if the frequency of crashes has decreased?

SimonMarquis commented 6 months ago

It seems like it did not change anything. New reports are coming up with this new version. To me, the only reason why there could be an error is if [maximumFlingVelocity](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewConfiguration#maximumFlingVelocity()) coming from the ViewConfiguration is already negative:

https://github.com/saket/telephoto/blob/7f88d6308209b04ffc0a576a069e3b5562bed71a/zoomable/src/commonMain/kotlin/me/saket/telephoto/zoomable/internal/transformable.kt#L155

The packing/unpacking of 2 floats in a long seems fine.

Given the uncommon distribution of devices having this bug, this could be an OS bug. And from the docs, nothing really prevents it from being negative after all.

Do you think we could try clamping these values?

          val maximumVelocity = currentValueOf(LocalViewConfiguration)
              .maximumFlingVelocity
              .coerceAtLeast(0F)
              .let { Velocity(it, it) }
saket commented 6 months ago

Oof. Considering that Velocity(Int.MAX_VALUE.toFloat(), Int.MAX_VALUE.toFloat()) was also causing the same crash, I don't think the values provided by ViewConfiguration are invalid...

I'll file a bug report.

saket commented 6 months ago

@SimonMarquis can you share more data about these devices, including their API versions?

SimonMarquis commented 6 months ago

Devices:

OS:

This OS distribution makes it look like a platform bug that has been fixed in Android 11.

saket commented 6 months ago

It very likely is! Interestingly, @jakewharton reminded me yesterday that we encountered a similar bug in Cash App a year ago. It's amazing that I've already run into it again.

screenshot_2024-05-01_at_1.35.17___pm.png

I'll file a bug report on D8, and think of a way to suppress these in telephoto.

Is Android 8 your min sdk version?

SimonMarquis commented 6 months ago

We have minSdk set to 24 (Android 7)

saket commented 6 months ago

I've filed a bug report on D8: https://issuetracker.google.com/u/1/issues/338381315.

saket commented 6 months ago

@SimonMarquis will you be able to share your apk here? https://issuetracker.google.com/issues/338381315#comment2

saket commented 3 months ago

@SimonMarquis are you observing these crashes in telephoto 0.12.1 by any chance?

SimonMarquis commented 3 months ago

I'll take a look at the reports next monday, but we had to downgrade from 0.12.0 to 0.11.2 because of a bigger crash during the first hours of the rollout:

Fatal Exception: java.lang.IllegalStateException
another transformation is already in progress

me.saket.telephoto.zoomable.RealZoomableState.animateSettlingOfZoomOnGestureEnd$zoomable_release (RealZoomableState.kt:124)
me.saket.telephoto.zoomable.ZoomableNode$onTransformStopped$1$1.invokeSuspend (Zoomable.kt:48)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:9)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:93)
androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch (AndroidUiDispatcher.android.kt:15)
androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch (AndroidUiDispatcher.android.kt:15)
androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run (AndroidUiDispatcher.android.kt:3)
android.os.Handler.handleCallback (Handler.java:938)
android.os.Handler.dispatchMessage (Handler.java:99)
android.os.Looper.loop (Looper.java:223)
android.app.ActivityThread.main (ActivityThread.java:7780)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:592)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:952)

I did not have time to investigate if this was a mis-use of the Composable or an error on the library itself.

saket commented 3 months ago

That doesn't look good. Can you share your usage of Zoomable*Image() and its APIs? That error message exposes a bug that was possibly already present in previous versions (including 0.11.2).

saket commented 3 months ago

Let's move our discussion for this new crash to https://github.com/saket/telephoto/issues/96

SimonMarquis commented 3 months ago

We are only using the Modifier.zoomable() APIs, like this:

This ZoomableImageGallery Composable is used inside a androidx.compose.foundation.pager.HorizontalPager. And the Image is a Design System wrapper around Coil's SubcomposeAsyncImage.

@Composable
fun ZoomableImageGallery(
    uri: String,
    modifier: Modifier = Modifier,
    index: Int? = null,
    pagerState: PagerState? = null,
) {
    var isImageOnError by remember { mutableStateOf(false) }
    var retryHash by remember { mutableIntStateOf(0) }
    val zoomState = rememberZoomableState(ZoomSpec(maxZoomFactor = 4.0f))

    val imageRequest = ImageRequest.Builder(LocalContext.current)
        .data(uri)
        .setParameter(key = "retry_hash", value = retryHash, memoryCacheKey = null)
        .build()

    Image(
        model = imageRequest,
        modifier = modifier
            .fillMaxSize()
            .zoomable(
                state = zoomState,
                onClick = { if (isImageOnError) retryHash++ }
            ),
        onState = { state -> isImageOnError = state is State.Error },
    )

    // Reset zoom state when the page is moved out of the window.
    val isVisible = index == pagerState?.settledPage
    LaunchedEffect(isVisible) {
        if (!isVisible) zoomState.resetZoom(withAnimation = false)
    }
}
saket commented 1 week ago

Hi @SimonMarquis, can you give 0.14.0 a try? https://github.com/saket/telephoto/releases/tag/0.14.0

SimonMarquis commented 1 week ago

I'll bump to 0.14.0 in our dev builds on monday, and if everything looks ok it will be part of the official release (next monday).