Stop trying to draw ripples while unattached In some codepaths drawing a ripple that hasn't been started yet, while the view is unattached, can lead to a crash. Safely cancel the ripple and stop drawing to avoid this problem. Fixes: b/377222399 Test: manual Change-Id: I51c2729f8208d44e4445bbf0f7c6b8790f33ebd7
diff --git a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt new file mode 100644 index 0000000..828090b --- /dev/null +++ b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleHostViewTest.kt
@@ -0,0 +1,74 @@ +/* + * Copyright 2025 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.material.ripple + +import android.graphics.RenderNode +import android.os.Build +import androidx.activity.ComponentActivity +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.filters.SdkSuppress +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Test for [RippleHostView] */ +@MediumTest +@RunWith(AndroidJUnit4::class) +class RippleHostViewTest { + + @get:Rule val rule = createAndroidComposeRule<ComponentActivity>() + + /** + * Test for b/377222399 + * + * Note, without the corresponding fix this test would only fail on Samsung devices, unless + * manually changing RippleDrawable.mRippleStyle.mRippleStyle to STYLE_SOLID through reflection. + */ + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) + @Test + fun doesNotDrawWhileUnattached() { + rule.runOnUiThread { + val activity = rule.activity + + // View is explicitly not attached + val rippleHostView = RippleHostView(activity) + + // Add a ripple while unattached + rippleHostView.addRipple( + PressInteraction.Press(Offset.Zero), + true, + Size(100f, 100f), + radius = 10, + color = Color.Red, + alpha = 0.4f, + onInvalidateRipple = {} + ) + + // Create a hardware backed canvas + val canvas = RenderNode("RippleHostViewTest").beginRecording() + + // Should not crash + rippleHostView.draw(canvas) + } + } +}
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt index d1f3bf4..ef949d5 100644 --- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt +++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
@@ -18,6 +18,7 @@ import android.content.Context import android.content.res.ColorStateList +import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable @@ -52,6 +53,15 @@ // noop } + override fun draw(canvas: Canvas) { + if (!isAttachedToWindow) { + // Cleanup any existing ripples if we added a ripple after being detached b/377222399 + disposeRipple() + return + } + super.draw(canvas) + } + override fun refreshDrawableState() { // We don't want the View to manage the drawable state, so avoid updating the ripple's // state (via View.mBackground) when we lose window focus, or other events.