Merge "Upgrade to Gradle 8.8-rc1" into androidx-main
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
index c82ead6..d184d2f 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
@@ -20,7 +20,7 @@
 
 // Minimum AGP version required
 internal val MIN_AGP_VERSION_REQUIRED_INCLUSIVE = AndroidPluginVersion(8, 0, 0)
-internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 5, 0).alpha(1)
+internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 6, 0).alpha(1)
 
 // Prefix for the build type baseline profile
 internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonMinified"
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt
new file mode 100644
index 0000000..d0e5b20
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2023 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.foundation.text
+
+import android.view.inputmethod.CursorAnchorInfo
+import android.view.inputmethod.ExtractedText
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.setFocusableContent
+import androidx.compose.foundation.text.handwriting.HandwritingBoundsVerticalOffset
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.input.InputMethodInterceptor
+import androidx.compose.foundation.text.input.internal.InputMethodManager
+import androidx.compose.foundation.text.input.internal.inputMethodManagerFactory
+import androidx.compose.foundation.text.matchers.isZero
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CoreTextFieldHandwritingBoundsTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+    private val fakeImm = object : InputMethodManager {
+        private var stylusHandwritingStartCount = 0
+
+        fun expectStylusHandwriting(started: Boolean) {
+            if (started) {
+                assertThat(stylusHandwritingStartCount).isEqualTo(1)
+                stylusHandwritingStartCount = 0
+            } else {
+                assertThat(stylusHandwritingStartCount).isZero()
+            }
+        }
+
+        override fun isActive(): Boolean = true
+
+        override fun restartInput() {}
+
+        override fun showSoftInput() {}
+
+        override fun hideSoftInput() {}
+
+        override fun updateExtractedText(token: Int, extractedText: ExtractedText) {}
+
+        override fun updateSelection(
+            selectionStart: Int,
+            selectionEnd: Int,
+            compositionStart: Int,
+            compositionEnd: Int
+        ) {}
+
+        override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {}
+
+        override fun startStylusHandwriting() {
+            ++stylusHandwritingStartCount
+        }
+    }
+
+    @Before
+    fun setup() {
+        // Test is only meaningful when stylusHandwriting is supported.
+        assumeTrue(isStylusHandwritingSupported)
+    }
+
+    @Test
+    fun coreTextField_stylusPointerInEditorBounds_focusAndStartHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        val editorTag1 = "CoreTextField1"
+        val editorTag2 = "CoreTextField2"
+
+        setContent {
+            Column(Modifier.safeContentPadding()) {
+                EditLine(Modifier.testTag(editorTag1))
+                EditLine(Modifier.testTag(editorTag2))
+            }
+        }
+
+        rule.onNodeWithTag(editorTag1).performStylusHandwriting()
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(editorTag1).assertIsFocused()
+        fakeImm.expectStylusHandwriting(true)
+    }
+
+    @Test
+    fun coreTextField_stylusPointerInOverlappingArea_focusedEditorStartHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        val editorTag1 = "CoreTextField1"
+        val editorTag2 = "CoreTextField2"
+        val spacerTag = "Spacer"
+
+        setContent {
+            Column(Modifier.safeContentPadding()) {
+                EditLine(Modifier.testTag(editorTag1))
+                Spacer(
+                    modifier = Modifier.fillMaxWidth()
+                        .height(HandwritingBoundsVerticalOffset)
+                        .testTag(spacerTag)
+                )
+                EditLine(Modifier.testTag(editorTag2))
+            }
+        }
+
+        rule.onNodeWithTag(editorTag2).requestFocus()
+        rule.waitForIdle()
+
+        // Spacer's height equals to HandwritingBoundsVerticalPadding, both editor will receive the
+        // event.
+        rule.onNodeWithTag(spacerTag).performStylusHandwriting()
+        rule.waitForIdle()
+
+        // Assert that focus didn't change, handwriting is started on the focused editor 2.
+        rule.onNodeWithTag(editorTag2).assertIsFocused()
+        fakeImm.expectStylusHandwriting(true)
+
+        rule.onNodeWithTag(editorTag1).requestFocus()
+        rule.onNodeWithTag(spacerTag).performStylusHandwriting()
+        rule.waitForIdle()
+
+        // Now handwriting is performed on the focused editor 1.
+        rule.onNodeWithTag(editorTag1).assertIsFocused()
+        fakeImm.expectStylusHandwriting(true)
+    }
+
+    @Composable
+    fun EditLine(modifier: Modifier = Modifier) {
+        var value by remember { mutableStateOf(TextFieldValue()) }
+        CoreTextField(
+            value = value,
+            onValueChange = { value = it },
+            modifier = modifier
+                .fillMaxWidth()
+                // make the size of TextFields equal to padding, so that touch bounds of editors
+                // in the same column/row are overlapping.
+                .height(HandwritingBoundsVerticalOffset)
+        )
+    }
+
+    private fun setContent(
+        extraItemForInitialFocus: Boolean = true,
+        content: @Composable () -> Unit
+    ) {
+        rule.setFocusableContent(extraItemForInitialFocus) {
+            inputMethodInterceptor.Content {
+                content()
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
index 966c702..5888282 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
@@ -101,11 +101,6 @@
     }
 
     private fun sendTouchEvent(action: Int) {
-        val positionInScreen = run {
-            val array = intArrayOf(0, 0)
-            root.view.getLocationOnScreen(array)
-            Offset(array[0].toFloat(), array[1].toFloat())
-        }
         val motionEvent = MotionEvent.obtain(
             /* downTime = */ downTime,
             /* eventTime = */ currentTime,
@@ -125,13 +120,13 @@
                     // test if it handles them properly (versus breaking here and we not knowing
                     // if Compose properly handles these values).
                     x = if (startOffset.isValid()) {
-                        positionInScreen.x + startOffset.x
+                        startOffset.x
                     } else {
                         Float.NaN
                     }
 
                     y = if (startOffset.isValid()) {
-                        positionInScreen.y + startOffset.y
+                        startOffset.y
                     } else {
                         Float.NaN
                     }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
index 6b69713..a6ac8fc 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.PointerInputModifierNode
@@ -93,11 +92,9 @@
         pointerInputNode.onCancelPointerInput()
     }
 
-    val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
-        detectStylusHandwriting {
-            callback()
-            composeImm.prepareStylusHandwritingDelegation()
-            return@detectStylusHandwriting true
-        }
+    val pointerInputNode = delegate(StylusHandwritingNode {
+        callback()
+        composeImm.prepareStylusHandwritingDelegation()
+        return@StylusHandwritingNode true
     })
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 50eccc5..8bd9033 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,8 +27,7 @@
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
-import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
-import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.handwriting.stylusHandwriting
 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -82,6 +81,7 @@
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.platform.LocalDensity
@@ -405,34 +405,6 @@
             textDragObserver = manager.touchSelectionObserver,
         )
         .pointerHoverIcon(textPointerIcon)
-        .then(
-            if (isStylusHandwritingSupported && writeable) {
-                Modifier.pointerInput(Unit) {
-                    detectStylusHandwriting {
-                        if (!state.hasFocus) {
-                            focusRequester.requestFocus()
-                        }
-                        // If this is a password field, we can't trigger handwriting.
-                        // The expected behavior is 1) request focus 2) show software keyboard.
-                        // Note: TextField will show software keyboard automatically when it
-                        // gain focus. 3) show a toast message telling that handwriting is not
-                        // supported for password fields. TODO(b/335294152)
-                        if (imeOptions.keyboardType != KeyboardType.Password) {
-                            // TextInputService is calling LegacyTextInputServiceAdapter under the
-                            // hood.  And because it's a public API, startStylusHandwriting is added
-                            // to legacyTextInputServiceAdapter instead.
-                            // startStylusHandwriting may be called before the actual input
-                            // session starts when the editor is not focused, this is handled
-                            // internally by the LegacyTextInputServiceAdapter.
-                            legacyTextInputServiceAdapter.startStylusHandwriting()
-                        }
-                        true
-                    }
-                }
-            } else {
-                Modifier
-            }
-        )
 
     val drawModifier = Modifier.drawBehind {
         state.layoutResult?.let { layoutResult ->
@@ -657,10 +629,32 @@
             imeAction = imeOptions.imeAction,
         )
 
+    val stylusHandwritingModifier = Modifier.stylusHandwriting(writeable) {
+        if (!state.hasFocus) {
+            focusRequester.requestFocus()
+        }
+        // If this is a password field, we can't trigger handwriting.
+        // The expected behavior is 1) request focus 2) show software keyboard.
+        // Note: TextField will show software keyboard automatically when it
+        // gain focus. 3) show a toast message telling that handwriting is not
+        // supported for password fields. TODO(b/335294152)
+        if (imeOptions.keyboardType != KeyboardType.Password) {
+            // TextInputService is calling LegacyTextInputServiceAdapter under the
+            // hood.  And because it's a public API, startStylusHandwriting is added
+            // to legacyTextInputServiceAdapter instead.
+            // startStylusHandwriting may be called before the actual input
+            // session starts when the editor is not focused, this is handled
+            // internally by the LegacyTextInputServiceAdapter.
+            legacyTextInputServiceAdapter.startStylusHandwriting()
+        }
+        true
+    }
+
     // Modifiers that should be applied to the outer text field container. Usually those include
     // gesture and semantics modifiers.
     val decorationBoxModifier = modifier
         .legacyTextInputAdapter(legacyTextInputServiceAdapter, state, manager)
+        .then(stylusHandwritingModifier)
         .then(focusModifier)
         .interceptDPadAndMoveFocus(state, focusManager)
         .previewKeyEventToDeselectOnBack(state, manager)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
index 3d4ba3b..381dbbf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
@@ -18,69 +18,194 @@
 
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusEventModifierNode
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
 import androidx.compose.ui.util.fastFirstOrNull
 
 /**
- * A utility function that detects stylus movements and calls the [onHandwritingSlopExceeded] when
+ * A modifier that detects stylus movements and calls the [onHandwritingSlopExceeded] when
  * it detects that stylus movement has exceeds the handwriting slop.
- * If [onHandwritingSlopExceeded] returns true, this method will consume the events and consider
+ * If [onHandwritingSlopExceeded] returns true, it will consume the events and consider
  * that the handwriting has successfully started. Otherwise, it'll stop monitoring the current
  * gesture.
+ * @param enabled whether this modifier is enabled, it's used for the case where the editor is
+ * readOnly or disabled.
+ * @param onHandwritingSlopExceeded the callback that's invoked when it detects stylus handwriting.
+ * The return value determines whether the handwriting is triggered or not. When it's true, this
+ * modifier will consume the pointer events.
  */
-internal suspend inline fun PointerInputScope.detectStylusHandwriting(
-    crossinline onHandwritingSlopExceeded: () -> Boolean
-) {
-    awaitEachGesture {
-        val firstDown =
-            awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+internal fun Modifier.stylusHandwriting(
+    enabled: Boolean,
+    onHandwritingSlopExceeded: () -> Boolean
+): Modifier = if (enabled && isStylusHandwritingSupported) {
+    this.then(StylusHandwritingElementWithNegativePadding(onHandwritingSlopExceeded))
+        .padding(
+            horizontal = HandwritingBoundsHorizontalOffset,
+            vertical = HandwritingBoundsVerticalOffset
+        )
+} else {
+    this
+}
 
-        val isStylus =
-            firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
-        if (!isStylus) {
-            return@awaitEachGesture
+private data class StylusHandwritingElementWithNegativePadding(
+    val onHandwritingSlopExceeded: () -> Boolean
+) : ModifierNodeElement<StylusHandwritingNodeWithNegativePadding>() {
+    override fun create(): StylusHandwritingNodeWithNegativePadding {
+        return StylusHandwritingNodeWithNegativePadding(onHandwritingSlopExceeded)
+    }
+
+    override fun update(node: StylusHandwritingNodeWithNegativePadding) {
+        node.onHandwritingSlopExceeded = onHandwritingSlopExceeded
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "stylusHandwriting"
+        properties["onHandwritingSlopExceeded"] = onHandwritingSlopExceeded
+    }
+}
+
+/**
+ * A stylus handwriting node with negative padding. This node should be  used in pair with a padding
+ * modifier. Together, they expands the touch bounds of the editor while keep its visual bounds the
+ * same.
+ * Note: this node is a temporary solution, ideally we don't need it.
+ */
+private class StylusHandwritingNodeWithNegativePadding(
+    onHandwritingSlopExceeded: () -> Boolean
+) : StylusHandwritingNode(onHandwritingSlopExceeded), LayoutModifierNode {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val paddingVerticalPx = HandwritingBoundsVerticalOffset.roundToPx()
+        val paddingHorizontalPx = HandwritingBoundsHorizontalOffset.roundToPx()
+        val newConstraint = constraints.offset(
+            2 * paddingHorizontalPx,
+            2 * paddingVerticalPx
+        )
+        val placeable = measurable.measure(newConstraint)
+
+        val height = placeable.height - paddingVerticalPx * 2
+        val width = placeable.width - paddingHorizontalPx * 2
+        return layout(width, height) {
+            placeable.place(-paddingHorizontalPx, -paddingVerticalPx)
         }
-        // Await the touch slop before long press timeout.
-        var exceedsTouchSlop: PointerInputChange? = null
-        // The stylus move must exceeds touch slop before long press timeout.
-        while (true) {
-            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
-            // The tracked pointer is consumed or lifted, stop tracking.
-            val change = pointerEvent.changes.fastFirstOrNull {
-                !it.isConsumed && it.id == firstDown.id && it.pressed
-            }
-            if (change == null) {
-                break
+    }
+
+    override fun sharePointerInputWithSiblings(): Boolean {
+        // Share events to siblings so that the expanded touch bounds won't block other elements
+        // surrounding the editor.
+        return true
+    }
+}
+
+internal open class StylusHandwritingNode(
+    var onHandwritingSlopExceeded: () -> Boolean
+) : DelegatingNode(), PointerInputModifierNode, FocusEventModifierNode {
+
+    private var focused = false
+
+    override fun onFocusEvent(focusState: FocusState) {
+        focused = focusState.isFocused
+    }
+
+    private val suspendingPointerInputModifierNode = delegate(SuspendingPointerInputModifierNode {
+        awaitEachGesture {
+            val firstDown =
+                awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+
+            val isStylus =
+                firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
+            if (!isStylus) {
+                return@awaitEachGesture
             }
 
-            val time = change.uptimeMillis - firstDown.uptimeMillis
-            if (time >= viewConfiguration.longPressTimeoutMillis) {
-                break
+            val isInBounds = firstDown.position.x >= 0 && firstDown.position.x < size.width &&
+                firstDown.position.y >= 0 && firstDown.position.y < size.height
+
+            // If the editor is focused or the first down is within the editor's bounds, we
+            // await the initial pass. This prioritize the focused editor over unfocused
+            // editor.
+            val pass = if (focused || isInBounds) {
+                PointerEventPass.Initial
+            } else {
+                PointerEventPass.Main
             }
 
-            val offset = change.position - firstDown.position
-            if (offset.getDistance() > viewConfiguration.handwritingSlop) {
-                exceedsTouchSlop = change
-                break
+            // Await the touch slop before long press timeout.
+            var exceedsTouchSlop: PointerInputChange? = null
+            // The stylus move must exceeds touch slop before long press timeout.
+            while (true) {
+                val pointerEvent = awaitPointerEvent(pass)
+                // The tracked pointer is consumed or lifted, stop tracking.
+                val change = pointerEvent.changes.fastFirstOrNull {
+                    !it.isConsumed && it.id == firstDown.id && it.pressed
+                }
+                if (change == null) {
+                    break
+                }
+
+                val time = change.uptimeMillis - firstDown.uptimeMillis
+                if (time >= viewConfiguration.longPressTimeoutMillis) {
+                    break
+                }
+
+                val offset = change.position - firstDown.position
+                if (offset.getDistance() > viewConfiguration.handwritingSlop) {
+                    exceedsTouchSlop = change
+                    break
+                }
+            }
+
+            if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
+                return@awaitEachGesture
+            }
+            exceedsTouchSlop.consume()
+
+            // Consume the remaining changes of this pointer.
+            while (true) {
+                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
+                val pointerChange = pointerEvent.changes.fastFirstOrNull {
+                    !it.isConsumed && it.id == firstDown.id && it.pressed
+                } ?: return@awaitEachGesture
+                pointerChange.consume()
             }
         }
+    })
 
-        if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
-            return@awaitEachGesture
-        }
-        exceedsTouchSlop.consume()
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {
+        suspendingPointerInputModifierNode.onPointerEvent(pointerEvent, pass, bounds)
+    }
 
-        // Consume the remaining changes of this pointer.
-        while (true) {
-            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
-            val pointerChange = pointerEvent.changes.fastFirstOrNull {
-                !it.isConsumed && it.id == firstDown.id && it.pressed
-            } ?: return@awaitEachGesture
-            pointerChange.consume()
-        }
+    override fun onCancelPointerInput() {
+        suspendingPointerInputModifierNode.onCancelPointerInput()
+    }
+
+    fun resetPointerInputHandler() {
+        suspendingPointerInputModifierNode.resetPointerInputHandler()
     }
 }
 
@@ -89,3 +214,9 @@
  *  and NOT for checking whether the IME supports handwriting.
  */
 internal expect val isStylusHandwritingSupported: Boolean
+
+/**
+ * The amount of the padding added to the handwriting bounds of an editor.
+ */
+internal val HandwritingBoundsVerticalOffset = 40.dp
+internal val HandwritingBoundsHorizontalOffset = 10.dp
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index eeddf40..5d02a03f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -29,7 +29,7 @@
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.KeyboardActionScope
 import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.StylusHandwritingNode
 import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.input.InputTransformation
 import androidx.compose.foundation.text.input.KeyboardActionHandler
@@ -225,41 +225,36 @@
                     detectTextFieldLongPressAndAfterDrag(requestFocus)
                 }
             }
-            // Note: when editable changes (enabled or readOnly changes) or keyboard type changes,
-            // this pointerInputModifier is reset. And we don't need to worry about cancel or launch
-            // the stylus handwriting detecting job.
-            if (isStylusHandwritingSupported && editable) {
-                 launch(start = CoroutineStart.UNDISPATCHED) {
-                    detectStylusHandwriting {
-                        if (!isFocused) {
-                            requestFocus()
-                        }
-                        // If this is a password field, we can't trigger handwriting.
-                        // The expected behavior is 1) request focus 2) show software keyboard.
-                        // Note: TextField will show software keyboard automatically when it
-                        // gain focus. 3) show a toast message telling that handwriting is not
-                        // supported for password fields. TODO(b/335294152)
-                        if (keyboardOptions.keyboardType != KeyboardType.Password) {
-                            // Send the handwriting start signal to platform.
-                            // The editor should send the signal when it is focused or is about
-                            // to gain focus, Here are more details:
-                            //   1) if the editor already has an active input session, the
-                            //   platform handwriting service should already listen to this flow
-                            //   and it'll start handwriting right away.
-                            //
-                            //   2) if the editor is not focused, but it'll be focused and
-                            //   create a new input session, one handwriting signal will be
-                            //   replayed when the platform collect this flow. And the platform
-                            //   should trigger handwriting accordingly.
-                            stylusHandwritingTrigger?.tryEmit(Unit)
-                        }
-                        return@detectStylusHandwriting true
-                    }
-                }
-            }
         }
     })
 
+    private val stylusHandwritingNode = delegate(StylusHandwritingNode {
+        if (!isFocused) {
+            requestFocus()
+        }
+
+        // If this is a password field, we can't trigger handwriting.
+        // The expected behavior is 1) request focus 2) show software keyboard.
+        // Note: TextField will show software keyboard automatically when it
+        // gain focus. 3) show a toast message telling that handwriting is not
+        // supported for password fields. TODO(b/335294152)
+        if (keyboardOptions.keyboardType != KeyboardType.Password) {
+            // Send the handwriting start signal to platform.
+            // The editor should send the signal when it is focused or is about
+            // to gain focus, Here are more details:
+            //   1) if the editor already has an active input session, the
+            //   platform handwriting service should already listen to this flow
+            //   and it'll start handwriting right away.
+            //
+            //   2) if the editor is not focused, but it'll be focused and
+            //   create a new input session, one handwriting signal will be
+            //   replayed when the platform collect this flow. And the platform
+            //   should trigger handwriting accordingly.
+            stylusHandwritingTrigger?.tryEmit(Unit)
+        }
+        return@StylusHandwritingNode true
+    })
+
     /**
      * The last enter event that was submitted to [interactionSource] from [dragAndDropNode]. We
      * need to keep a reference to this event to send a follow-up exit event.
@@ -458,6 +453,7 @@
 
         if (textFieldSelectionState != previousTextFieldSelectionState) {
             pointerInputNode.resetPointerInputHandler()
+            stylusHandwritingNode.resetPointerInputHandler()
             if (isAttached) {
                 textFieldSelectionState.receiveContentConfiguration =
                     receiveContentConfigurationProvider
@@ -466,6 +462,7 @@
 
         if (interactionSource != previousInteractionSource) {
             pointerInputNode.resetPointerInputHandler()
+            stylusHandwritingNode.resetPointerInputHandler()
         }
     }
 
@@ -604,6 +601,7 @@
             disposeInputSession()
             textFieldState.collapseSelectionToMax()
         }
+        stylusHandwritingNode.onFocusEvent(focusState)
     }
 
     override fun onAttach() {
@@ -625,10 +623,12 @@
         pass: PointerEventPass,
         bounds: IntSize
     ) {
+        stylusHandwritingNode.onPointerEvent(pointerEvent, pass, bounds)
         pointerInputNode.onPointerEvent(pointerEvent, pass, bounds)
     }
 
     override fun onCancelPointerInput() {
+        stylusHandwritingNode.onCancelPointerInput()
         pointerInputNode.onCancelPointerInput()
     }
 
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index 324023b..aff828d 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -136,3 +136,13 @@
 android {
     namespace "androidx.compose.material.icons.extended"
 }
+
+afterEvaluate {
+    // Workaround for b/337776938
+    tasks.named("lintAnalyzeDebugAndroidTest").configure {
+        it.dependsOn("generateTestFilesDebugAndroidTest")
+    }
+    tasks.named("generateDebugAndroidTestLintModel").configure {
+        it.dependsOn("generateTestFilesDebugAndroidTest")
+    }
+}
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index e34eca2..d8802c0 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-beta01)" variant="all" version="8.3.0-beta01">
+<issues format="6" by="lint 8.5.0-alpha03" type="baseline" client="gradle" dependencies="false" name="AGP (8.5.0-alpha03)" variant="all" version="8.5.0-alpha03">
 
     <issue
         id="NewApi"
@@ -12,7 +12,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -30,7 +30,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -48,7 +48,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
         <location
@@ -75,7 +75,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt
new file mode 100644
index 0000000..1fac1b6
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.ui.node
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.DrawerValue
+import androidx.compose.material.ModalDrawer
+import androidx.compose.material.rememberDrawerState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SharePointerInputWithSiblingTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun Drawer_drawerContentSharePointerInput_cantClickContent() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            val drawerState = rememberDrawerState(DrawerValue.Open)
+
+            ModalDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier
+                        .fillMaxSize()
+                        .testTag("box1")
+                        .testPointerInput(sharePointerInputWithSibling = true) {
+                            box1Clicked = true
+                        }
+                    )
+                },
+                content = {
+                    Box(Modifier
+                        .fillMaxSize()
+                        .testTag("box2")
+                        .testPointerInput {
+                            box2Clicked = true
+                        }
+                    )
+                }
+            )
+        }
+
+        rule.onNodeWithTag("box1").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isFalse()
+    }
+
+    @Test
+    fun stackedBox_doSharePointer() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box2")
+                    .testPointerInput(sharePointerInputWithSibling = true) {
+                        box2Clicked = true
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_parentDisallowShare_doSharePointerWithSibling() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier
+                .size(50.dp)
+                .testPointerInput(sharePointerInputWithSibling = false)
+            ) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box2")
+                    .testPointerInput(sharePointerInputWithSibling = true) {
+                        box2Clicked = true
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_doSharePointerWithCousin() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()) {
+                    Box(Modifier
+                        .fillMaxSize()
+                        .testTag("box2")
+                        .testPointerInput(sharePointerInputWithSibling = true) {
+                            box2Clicked = true
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_parentDisallowShare_notSharePointerWithCousin() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier.fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()
+                    .testPointerInput(sharePointerInputWithSibling = false)
+                ) {
+                    Box(Modifier.fillMaxSize()
+                        .testTag("box2")
+                        .testPointerInput(sharePointerInputWithSibling = true) {
+                            box2Clicked = true
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isFalse()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_doSharePointer_untilFirstBoxDisallowShare() {
+        var box1Clicked = false
+        var box2Clicked = false
+        var box3Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier.fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()
+                    .testTag("box2")
+                    .testPointerInput(sharePointerInputWithSibling = false) {
+                        box2Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()
+                    .testTag("box3")
+                    .testPointerInput(sharePointerInputWithSibling = true) {
+                        box3Clicked = true
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box3").performClick()
+        assertThat(box1Clicked).isFalse()
+        assertThat(box2Clicked).isTrue()
+        assertThat(box3Clicked).isTrue()
+    }
+}
+
+private fun Modifier.testPointerInput(
+    sharePointerInputWithSibling: Boolean = false,
+    onPointerEvent: () -> Unit = {}
+): Modifier = this.then(TestPointerInputElement(sharePointerInputWithSibling, onPointerEvent))
+
+private data class TestPointerInputElement(
+    val sharePointerInputWithSibling: Boolean,
+    val onPointerEvent: () -> Unit
+) : ModifierNodeElement<TestPointerInputNode>() {
+    override fun create(): TestPointerInputNode {
+        return TestPointerInputNode(sharePointerInputWithSibling, onPointerEvent)
+    }
+
+    override fun update(node: TestPointerInputNode) {
+        node.sharePointerInputWithSibling = sharePointerInputWithSibling
+        node.onPointerEvent = onPointerEvent
+    }
+}
+
+private class TestPointerInputNode(
+    var sharePointerInputWithSibling: Boolean,
+    var onPointerEvent: () -> Unit
+) : Modifier.Node(), PointerInputModifierNode {
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {
+        onPointerEvent.invoke()
+    }
+
+    override fun onCancelPointerInput() { }
+
+    override fun sharePointerInputWithSiblings(): Boolean {
+        return sharePointerInputWithSibling
+    }
+}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt
new file mode 100644
index 0000000..36b2ec8
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 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.ui.node
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.unit.IntSize
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class HitTestSharePointerInputWithSiblingTest {
+    @Test
+    fun hitTest_sharePointerWithSibling() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier1))
+    }
+
+    @Test
+    fun hitTest_sharePointerWithSibling_utilFirstNodeNotSharing() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier3, pointerInputModifier2))
+    }
+
+    @Test
+    fun hitTest_sharePointerWithSibling_whenParentDisallowShare() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier()) {
+                childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+                childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        // The parent node doesn't share pointer events, the two children can still share events.
+        assertThat(hit).isEqualTo(
+            listOf(pointerInputModifier1, pointerInputModifier3, pointerInputModifier2)
+        )
+    }
+
+    @Test
+    fun hitTest_sharePointerWithSiblingTrue_shareWithCousin() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1) {
+                childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier1))
+    }
+
+    @Test
+    fun hitTest_parentDisallowShare_notShareWithCousin() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier2.toModifier()) {
+                childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        // PointerInputModifier1 can't receive events because pointerInputModifier2 doesn't share.
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier3))
+    }
+
+    @Test
+    fun hitTest_sharePointerWithCousin_untilFirstNodeNotSharing() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1) {
+                childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+            }
+            childNode(0, 0, 1, 1) {
+                childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier3, pointerInputModifier2))
+    }
+}
+
+private fun LayoutNode(
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+    block: LayoutNode.() -> Unit
+): LayoutNode {
+    val root = LayoutNode(left, top, right, bottom).apply {
+        attach(MockOwner())
+    }
+
+    block.invoke(root)
+    return root
+}
+
+private fun LayoutNode.childNode(
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+    modifier: Modifier = Modifier,
+    block: LayoutNode.() -> Unit = {}
+): LayoutNode {
+    val layoutNode = LayoutNode(left, top, right, bottom, modifier)
+    add(layoutNode)
+    layoutNode.onNodePlaced()
+    block.invoke(layoutNode)
+    return layoutNode
+}
+
+private fun FakePointerInputModifierNode.toModifier(): Modifier {
+    return object : ModifierNodeElement<FakePointerInputModifierNode>() {
+        override fun create(): FakePointerInputModifierNode = this@toModifier
+
+        override fun update(node: FakePointerInputModifierNode) { }
+
+        override fun hashCode(): Int {
+            return if ([email protected]) 1 else 0
+        }
+
+        override fun equals(other: Any?): Boolean {
+           return [email protected]
+        }
+    }
+}
+
+private class FakePointerInputModifierNode(
+    var sharePointerWithSiblings: Boolean = false
+) : Modifier.Node(), PointerInputModifierNode {
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {}
+
+    override fun onCancelPointerInput() {}
+
+    override fun sharePointerInputWithSiblings(): Boolean = this.sharePointerWithSiblings
+}
+
+private fun LayoutNode.hitTest(
+    pointerPosition: Offset,
+    hitPointerInputFilters: MutableList<Modifier.Node>,
+    isTouchEvent: Boolean = false
+) {
+    val hitTestResult = HitTestResult()
+    hitTest(pointerPosition, hitTestResult, isTouchEvent)
+    hitPointerInputFilters.addAll(hitTestResult)
+}
+
+private fun LayoutNode.onNodePlaced() = measurePassDelegate.onNodePlaced()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 91c2d87..45c7b33 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -190,11 +190,21 @@
 internal inline fun DelegatableNode.visitLocalDescendants(
     mask: Int,
     block: (Modifier.Node) -> Unit
+) = visitLocalDescendants(
+    mask = mask,
+    includeSelf = false,
+    block = block
+)
+
+internal inline fun DelegatableNode.visitLocalDescendants(
+    mask: Int,
+    includeSelf: Boolean = false,
+    block: (Modifier.Node) -> Unit
 ) {
     checkPrecondition(node.isAttached) { "visitLocalDescendants called on an unattached node" }
     val self = node
     if (self.aggregateChildKindSet and mask == 0) return
-    var next = self.child
+    var next = if (includeSelf) self else self.child
     while (next != null) {
         if (next.kindSet and mask != 0) {
             block(next)
@@ -217,6 +227,13 @@
     }
 }
 
+internal inline fun <reified T> DelegatableNode.visitSelfAndLocalDescendants(
+    type: NodeKind<T>,
+    block: (T) -> Unit
+) = visitLocalDescendants(mask = type.mask, includeSelf = true) {
+    it.dispatchForKind(type, block)
+}
+
 internal inline fun <reified T> DelegatableNode.visitLocalDescendants(
     type: NodeKind<T>,
     block: (T) -> Unit
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
index c41e989..0203f66 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
@@ -40,6 +40,8 @@
     override var size: Int = 0
         private set
 
+    var shouldSharePointerInputWithSibling = true
+
     /**
      * `true` when there has been a direct hit within touch bounds ([hit] called) or
      * `false` otherwise.
@@ -95,6 +97,9 @@
      */
     fun hit(node: Modifier.Node, isInLayer: Boolean, childHitTest: () -> Unit) {
         hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
+        if (node.coordinator?.shouldSharePointerInputWithSiblings() == false) {
+            shouldSharePointerInputWithSibling = false
+        }
     }
 
     /**
@@ -238,6 +243,7 @@
     fun clear() {
         hitDepth = -1
         resizeToHitDepth()
+        shouldSharePointerInputWithSibling = true
     }
 
     private inner class HitTestResultIterator(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index 26d852c..3b79baa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -240,9 +240,7 @@
                         val continueHitTest: Boolean
                         if (!wasHit) {
                             continueHitTest = true
-                        } else if (
-                            child.outerCoordinator.shouldSharePointerInputWithSiblings()
-                        ) {
+                        } else if (hitTestResult.shouldSharePointerInputWithSibling) {
                             hitTestResult.acceptHits()
                             continueHitTest = true
                         } else {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 8a6b586..c3aa5ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1215,7 +1215,10 @@
         val start = headNode(Nodes.PointerInput.includeSelfInTraversal) ?: return false
 
         if (start.isAttached) {
-            start.visitLocalDescendants(Nodes.PointerInput) {
+            // We have to check both the self and local descendants, because the `start` can also
+            // be a `PointerInputModifierNode` (when the first modifier node on the LayoutNode is
+            // a `PointerInputModifierNode`).
+            start.visitSelfAndLocalDescendants(Nodes.PointerInput) {
                 if (it.sharePointerInputWithSiblings()) return true
             }
         }
diff --git a/development/update_studio.sh b/development/update_studio.sh
index ef74a284..324322f 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -7,8 +7,8 @@
 
 # Versions that the user should update when running this script
 echo Getting Studio version and link
-AGP_VERSION=${1:-8.4.0-alpha12}
-STUDIO_VERSION_STRING=${2:-"Android Studio Jellyfish | 2023.3.1 Canary 12"}
+AGP_VERSION=${1:-8.5.0-alpha06}
+STUDIO_VERSION_STRING=${2:-"Android Studio Koala | 2024.1.1 Canary 6"}
 
 # Get studio version number from version name
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep "<iframe " | sed "s/.* src=\"\([^\"]*\)\".*/\1/g"`
diff --git a/gradle.properties b/gradle.properties
index 7a42aa7..68fc0ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,6 +21,7 @@
 # fullsdk-linux/**/package.xml -> b/291331139
 org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml
 
+android.javaCompile.suppressSourceTargetDeprecationWarning=true
 android.lint.baselineOmitLineNumbers=true
 android.lint.printStackTrace=true
 android.builder.sdkDownload=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9c01869..da3f63c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,13 +2,13 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "8.4.0-alpha12"
+androidGradlePlugin = "8.5.0-alpha06"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "31.4.0-alpha12"
+androidLint = "31.5.0-alpha06"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2023.3.1.12"
+androidStudio = "2024.1.1.4"
 # -----------------------------------------------------------------------------
 
 androidGradlePluginMin = "7.0.4"
diff --git a/kruth/kruth/api/current.ignore b/kruth/kruth/api/current.ignore
index 2fc7dd5..5589f1d 100644
--- a/kruth/kruth/api/current.ignore
+++ b/kruth/kruth/api/current.ignore
@@ -73,8 +73,6 @@
     Removed class androidx.kruth.PrimitiveDoubleArraySubject.DoubleArrayAsIterable
 RemovedClass: androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable:
     Removed class androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable
-RemovedClass: androidx.kruth.TableSubject:
-    Removed class androidx.kruth.TableSubject
 RemovedClass: androidx.kruth.Truth:
     Removed class androidx.kruth.Truth
 RemovedClass: androidx.kruth.TruthJUnit:
diff --git a/kruth/kruth/api/current.txt b/kruth/kruth/api/current.txt
index 857e856..26a1b93 100644
--- a/kruth/kruth/api/current.txt
+++ b/kruth/kruth/api/current.txt
@@ -201,6 +201,7 @@
     method public static <T> androidx.kruth.GuavaOptionalSubject<T> assertThat(com.google.common.base.Optional<T>? actual);
     method public static <K, V> androidx.kruth.MultimapSubject<K,V> assertThat(com.google.common.collect.Multimap<K,V> actual);
     method public static <T> androidx.kruth.MultisetSubject<T> assertThat(com.google.common.collect.Multiset<T> actual);
+    method public static <R, C, V> androidx.kruth.TableSubject<R,C,V> assertThat(com.google.common.collect.Table<R,C,V> actual);
     method public static androidx.kruth.ClassSubject assertThat(Class<?> actual);
     method public static androidx.kruth.BigDecimalSubject assertThat(java.math.BigDecimal actual);
   }
@@ -418,6 +419,21 @@
     method public SubjectT createSubject(androidx.kruth.FailureMetadata metadata, ActualT? actual);
   }
 
+  public final class TableSubject<R, C, V> extends androidx.kruth.Subject<com.google.common.collect.Table<R,C,V>> {
+    method public void contains(R rowKey, C columnKey);
+    method public void containsCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void containsCell(R rowKey, C colKey, V value);
+    method public void containsColumn(C columnKey);
+    method public void containsRow(R rowKey);
+    method public void containsValue(V value);
+    method public void doesNotContain(R rowKey, C columnKey);
+    method public void doesNotContainCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void doesNotContainCell(R rowKey, C colKey, V value);
+    method public void hasSize(int expectedSize);
+    method public void isEmpty();
+    method public void isNotEmpty();
+  }
+
   public class ThrowableSubject<T extends java.lang.Throwable> extends androidx.kruth.Subject<T> {
     ctor protected ThrowableSubject(androidx.kruth.FailureMetadata metadata, T? actual);
     method public final androidx.kruth.ThrowableSubject<java.lang.Throwable> hasCauseThat();
diff --git a/kruth/kruth/api/restricted_current.ignore b/kruth/kruth/api/restricted_current.ignore
index 2fc7dd5..5589f1d 100644
--- a/kruth/kruth/api/restricted_current.ignore
+++ b/kruth/kruth/api/restricted_current.ignore
@@ -73,8 +73,6 @@
     Removed class androidx.kruth.PrimitiveDoubleArraySubject.DoubleArrayAsIterable
 RemovedClass: androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable:
     Removed class androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable
-RemovedClass: androidx.kruth.TableSubject:
-    Removed class androidx.kruth.TableSubject
 RemovedClass: androidx.kruth.Truth:
     Removed class androidx.kruth.Truth
 RemovedClass: androidx.kruth.TruthJUnit:
diff --git a/kruth/kruth/api/restricted_current.txt b/kruth/kruth/api/restricted_current.txt
index 90ec259..4d8c416 100644
--- a/kruth/kruth/api/restricted_current.txt
+++ b/kruth/kruth/api/restricted_current.txt
@@ -201,6 +201,7 @@
     method public static <T> androidx.kruth.GuavaOptionalSubject<T> assertThat(com.google.common.base.Optional<T>? actual);
     method public static <K, V> androidx.kruth.MultimapSubject<K,V> assertThat(com.google.common.collect.Multimap<K,V> actual);
     method public static <T> androidx.kruth.MultisetSubject<T> assertThat(com.google.common.collect.Multiset<T> actual);
+    method public static <R, C, V> androidx.kruth.TableSubject<R,C,V> assertThat(com.google.common.collect.Table<R,C,V> actual);
     method public static androidx.kruth.ClassSubject assertThat(Class<?> actual);
     method public static androidx.kruth.BigDecimalSubject assertThat(java.math.BigDecimal actual);
   }
@@ -419,6 +420,21 @@
     method public SubjectT createSubject(androidx.kruth.FailureMetadata metadata, ActualT? actual);
   }
 
+  public final class TableSubject<R, C, V> extends androidx.kruth.Subject<com.google.common.collect.Table<R,C,V>> {
+    method public void contains(R rowKey, C columnKey);
+    method public void containsCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void containsCell(R rowKey, C colKey, V value);
+    method public void containsColumn(C columnKey);
+    method public void containsRow(R rowKey);
+    method public void containsValue(V value);
+    method public void doesNotContain(R rowKey, C columnKey);
+    method public void doesNotContainCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void doesNotContainCell(R rowKey, C colKey, V value);
+    method public void hasSize(int expectedSize);
+    method public void isEmpty();
+    method public void isNotEmpty();
+  }
+
   public class ThrowableSubject<T extends java.lang.Throwable> extends androidx.kruth.Subject<T> {
     ctor protected ThrowableSubject(androidx.kruth.FailureMetadata metadata, T? actual);
     method public final androidx.kruth.ThrowableSubject<java.lang.Throwable> hasCauseThat();
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
index ea1fbc7..9b8c123 100644
--- a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
@@ -19,6 +19,7 @@
 import com.google.common.base.Optional
 import com.google.common.collect.Multimap
 import com.google.common.collect.Multiset
+import com.google.common.collect.Table
 import java.math.BigDecimal
 
 fun assertThat(actual: Class<*>): ClassSubject =
@@ -35,3 +36,6 @@
 
 fun <K, V> assertThat(actual: Multimap<K, V>): MultimapSubject<K, V> =
     MultimapSubject(actual = actual)
+
+fun <R, C, V> assertThat(actual: Table<R, C, V>): TableSubject<R, C, V> =
+    TableSubject(actual = actual)
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
index 891e0a2..e4030e6 100644
--- a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
@@ -19,6 +19,7 @@
 import com.google.common.base.Optional
 import com.google.common.collect.Multimap
 import com.google.common.collect.Multiset
+import com.google.common.collect.Table
 import java.math.BigDecimal
 
 internal actual interface PlatformStandardSubjectBuilder {
@@ -28,6 +29,7 @@
     fun that(actual: BigDecimal): BigDecimalSubject
     fun <T> that(actual: Multiset<T>): MultisetSubject<T>
     fun <K, V> that(actual: Multimap<K, V>): MultimapSubject<K, V>
+    fun <R, C, V> that(actual: Table<R, C, V>): TableSubject<R, C, V>
 }
 
 internal actual class PlatformStandardSubjectBuilderImpl actual constructor(
@@ -48,4 +50,7 @@
 
     override fun <K, V> that(actual: Multimap<K, V>): MultimapSubject<K, V> =
         MultimapSubject(actual = actual, metadata = metadata)
+
+    override fun <R, C, V> that(actual: Table<R, C, V>): TableSubject<R, C, V> =
+        TableSubject(actual = actual, metadata = metadata)
 }
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt
new file mode 100644
index 0000000..80f873e
--- /dev/null
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.kruth
+
+import androidx.kruth.Fact.Companion.fact
+import androidx.kruth.Fact.Companion.simpleFact
+import com.google.common.collect.Table
+import com.google.common.collect.Table.Cell
+import com.google.common.collect.Tables.immutableCell
+
+class TableSubject<R, C, V> internal constructor(
+    actual: Table<R, C, V>,
+    metadata: FailureMetadata = FailureMetadata(),
+) : Subject<Table<R, C, V>>(actual, metadata, typeDescriptionOverride = null) {
+
+    /** Fails if the table is not empty. */
+    fun isEmpty() {
+        requireNonNull(actual)
+
+        if (!actual.isEmpty) {
+            failWithActual(simpleFact("expected to be empty"))
+        }
+    }
+
+    /** Fails if the table is empty. */
+    fun isNotEmpty() {
+        requireNonNull(actual)
+
+        if (actual.isEmpty) {
+            failWithoutActual(simpleFact("expected not to be empty"))
+        }
+    }
+
+    /** Fails if the table does not have the given size. */
+    fun hasSize(expectedSize: Int) {
+        require(expectedSize >= 0) { "expectedSize($expectedSize) must be >= 0" }
+        requireNonNull(actual)
+
+        check("size()").that(actual.size()).isEqualTo(expectedSize)
+    }
+
+    /** Fails if the table does not contain a mapping for the given row key and column key. */
+    fun contains(rowKey: R, columnKey: C) {
+        requireNonNull(actual)
+
+        if (!actual.contains(rowKey, columnKey)) {
+            failWithActual(
+                simpleFact("expected to contain mapping for row-column key pair"),
+                fact("row key", rowKey),
+                fact("column key", columnKey),
+            )
+        }
+    }
+
+    /** Fails if the table contains a mapping for the given row key and column key. */
+    fun doesNotContain(rowKey: R, columnKey: C) {
+        requireNonNull(actual)
+
+        if (actual.contains(rowKey, columnKey)) {
+            failWithoutActual(
+                simpleFact("expected not to contain mapping for row-column key pair"),
+                fact("row key", rowKey),
+                fact("column key", columnKey),
+                fact("but contained value", actual[rowKey, columnKey]),
+                fact("full contents", actual),
+            )
+        }
+    }
+
+    /** Fails if the table does not contain the given cell. */
+    fun containsCell(rowKey: R, colKey: C, value: V) {
+        containsCell(immutableCell(rowKey, colKey, value))
+    }
+
+    /** Fails if the table does not contain the given cell. */
+    fun containsCell(cell: Cell<R, C, V>?) {
+        requireNonNull(cell)
+        requireNonNull(actual)
+
+        checkNoNeedToDisplayBothValues("cellSet()")
+            .that(actual.cellSet())
+            .contains(cell)
+    }
+
+    /** Fails if the table contains the given cell. */
+    fun doesNotContainCell(rowKey: R, colKey: C, value: V) {
+        doesNotContainCell(immutableCell(rowKey, colKey, value))
+    }
+
+    /** Fails if the table contains the given cell. */
+    fun doesNotContainCell(cell: Cell<R, C, V>?) {
+        requireNonNull(cell)
+        requireNonNull(actual)
+
+        checkNoNeedToDisplayBothValues("cellSet()")
+            .that(actual.cellSet())
+            .doesNotContain(cell)
+    }
+
+    /** Fails if the table does not contain the given row key. */
+    fun containsRow(rowKey: R) {
+        requireNonNull(actual)
+
+        check("rowKeySet()").that(actual.rowKeySet()).contains(rowKey)
+    }
+
+    /** Fails if the table does not contain the given column key. */
+    fun containsColumn(columnKey: C) {
+        requireNonNull(actual)
+
+        check("columnKeySet()").that(actual.columnKeySet()).contains(columnKey)
+    }
+
+    /** Fails if the table does not contain the given value. */
+    fun containsValue(value: V) {
+        requireNonNull(actual)
+
+        check("values()").that(actual.values()).contains(value)
+    }
+}
diff --git a/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt
new file mode 100644
index 0000000..a2a1157
--- /dev/null
+++ b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.kruth
+
+import com.google.common.collect.ImmutableTable
+import com.google.common.collect.Tables.immutableCell
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class TableSubjectTest {
+
+    @Test
+    fun tableIsEmpty() {
+        val table = ImmutableTable.of<String, String, String>()
+        assertThat(table).isEmpty()
+    }
+
+    @Test
+    fun tableIsEmptyWithFailure() {
+        val table = ImmutableTable.of(1, 5, 7)
+        assertFailsWith<AssertionError> {
+            assertThat(table).isEmpty()
+        }
+    }
+
+    @Test
+    fun tableIsNotEmpty() {
+        val table = ImmutableTable.of(1, 5, 7)
+        assertThat(table).isNotEmpty()
+    }
+
+    @Test
+    fun tableIsNotEmptyWithFailure() {
+        val table = ImmutableTable.of<Int, Int, Int>()
+        assertFailsWith<AssertionError> {
+            assertThat(table).isNotEmpty()
+        }
+    }
+
+    @Test
+    fun hasSize() {
+        assertThat(ImmutableTable.of(1, 2, 3)).hasSize(1)
+    }
+
+    @Test
+    fun hasSizeZero() {
+        assertThat(ImmutableTable.of<Any, Any, Any>()).hasSize(0)
+    }
+
+    @Test
+    fun hasSizeNegative() {
+        assertFailsWith<IllegalArgumentException> {
+            assertThat(ImmutableTable.of(1, 2, 3)).hasSize(-1)
+        }
+    }
+
+    @Test
+    fun contains() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).contains("row", "col")
+    }
+
+    @Test
+    fun containsFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+
+        assertFailsWith<AssertionError> {
+            assertThat(table).contains("row", "otherCol")
+        }
+    }
+
+    @Test
+    fun doesNotContain() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).doesNotContain("row", "row")
+        assertThat(table).doesNotContain("col", "row")
+        assertThat(table).doesNotContain("col", "col")
+        assertThat(table).doesNotContain(null, null)
+    }
+
+    @Test
+    fun doesNotContainFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertFailsWith<AssertionError> {
+            assertThat(table).doesNotContain("row", "col")
+        }
+    }
+
+    @Test
+    fun containsCell() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).containsCell("row", "col", "val")
+        assertThat(table).containsCell(immutableCell("row", "col", "val"))
+    }
+
+    @Test
+    fun containsCellFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertFailsWith<AssertionError> {
+            assertThat(table).containsCell("row", "row", "val")
+        }
+    }
+
+    @Test
+    fun doesNotContainCell() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).doesNotContainCell("row", "row", "val")
+        assertThat(table).doesNotContainCell("col", "row", "val")
+        assertThat(table).doesNotContainCell("col", "col", "val")
+        assertThat(table).doesNotContainCell(null, null, null)
+        assertThat(table).doesNotContainCell(immutableCell("row", "row", "val"))
+        assertThat(table).doesNotContainCell(immutableCell("col", "row", "val"))
+        assertThat(table).doesNotContainCell(immutableCell("col", "col", "val"))
+        assertThat(table).doesNotContainCell(immutableCell(null, null, null))
+    }
+
+    @Test
+    fun doesNotContainCellFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertFailsWith<AssertionError> {
+            assertThat(table).doesNotContainCell("row", "col", "val")
+        }
+    }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 6057cc1..9a36c5f 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -487,7 +487,7 @@
 
             // Builtin R8 desugaring, such as rewriting compare calls (see b/36390874)
             if (owner.startsWith("java.") &&
-                DesugaredMethodLookup.isDesugared(owner, name, desc, context.sourceSetType)) {
+                DesugaredMethodLookup.isDesugaredMethod(owner, name, desc, context.sourceSetType)) {
                 return
             }
 
@@ -573,7 +573,7 @@
             api: Int
         ): LintFix? {
             val callPsi = call.sourcePsi ?: return null
-            if (isKotlin(callPsi)) {
+            if (isKotlin(callPsi.language)) {
                 // We only support Java right now.
                 return null
             }
diff --git a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
index a9f7fbd..a773acc 100644
--- a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
@@ -59,7 +59,7 @@
                 return
             }
 
-            if (!isKotlin(node)) return
+            if (!isKotlin(node.language)) return
             if (!node.isInterface) return
             if (node.annotatedWithAnyOf(
                     // If the interface is not stable, it doesn't need the annotation
diff --git a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
index 6ed71ee..bf0227e 100644
--- a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
@@ -40,7 +40,8 @@
 
     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
         override fun visitAnnotation(node: UAnnotation) {
-            if (isJava(node.sourcePsi)) {
+            val element = node.sourcePsi
+            if (element != null && isJava(element.language)) {
                 checkForAnnotation(node, "NotNull", "NonNull")
                 checkForAnnotation(node, "Nullable", "Nullable")
             }
diff --git a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
index 36cbf9cd..9eaf45c 100644
--- a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
@@ -86,7 +86,9 @@
             // here, but that points to impl classes in its hierarchy which leads to
             // class loading trouble.
             val sourcePsi = element.sourcePsi
-            if (isKotlin(sourcePsi) && sourcePsi?.parent?.toString() == "CONSTRUCTOR_CALLEE") {
+            if (sourcePsi != null &&
+                isKotlin(sourcePsi.language) &&
+                sourcePsi.parent?.toString() == "CONSTRUCTOR_CALLEE") {
                 return
             }
         }