1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.input.pointer
18 
19 import android.content.Context
20 import android.os.Build
21 import android.view.InputDevice
22 import android.view.MotionEvent
23 import android.view.MotionEvent.ACTION_BUTTON_PRESS
24 import android.view.MotionEvent.ACTION_BUTTON_RELEASE
25 import android.view.MotionEvent.ACTION_CANCEL
26 import android.view.MotionEvent.ACTION_DOWN
27 import android.view.MotionEvent.ACTION_HOVER_ENTER
28 import android.view.MotionEvent.ACTION_HOVER_EXIT
29 import android.view.MotionEvent.ACTION_HOVER_MOVE
30 import android.view.MotionEvent.ACTION_MOVE
31 import android.view.MotionEvent.ACTION_OUTSIDE
32 import android.view.MotionEvent.ACTION_POINTER_DOWN
33 import android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT
34 import android.view.MotionEvent.ACTION_POINTER_UP
35 import android.view.MotionEvent.ACTION_SCROLL
36 import android.view.MotionEvent.ACTION_UP
37 import android.view.MotionEvent.PointerCoords
38 import android.view.MotionEvent.TOOL_TYPE_FINGER
39 import android.view.MotionEvent.TOOL_TYPE_MOUSE
40 import android.view.View
41 import android.view.ViewGroup
42 import androidx.activity.ComponentActivity
43 import androidx.annotation.RequiresApi
44 import androidx.compose.foundation.gestures.awaitFirstDown
45 import androidx.compose.foundation.gestures.detectTapGestures
46 import androidx.compose.foundation.layout.Arrangement
47 import androidx.compose.foundation.layout.Box
48 import androidx.compose.foundation.layout.Column
49 import androidx.compose.foundation.layout.fillMaxSize
50 import androidx.compose.foundation.layout.height
51 import androidx.compose.foundation.layout.offset
52 import androidx.compose.foundation.layout.padding
53 import androidx.compose.foundation.layout.requiredSize
54 import androidx.compose.foundation.layout.size
55 import androidx.compose.foundation.layout.width
56 import androidx.compose.foundation.layout.wrapContentSize
57 import androidx.compose.runtime.Composable
58 import androidx.compose.runtime.getValue
59 import androidx.compose.runtime.mutableStateOf
60 import androidx.compose.runtime.remember
61 import androidx.compose.runtime.rememberCoroutineScope
62 import androidx.compose.runtime.setValue
63 import androidx.compose.runtime.snapshots.Snapshot
64 import androidx.compose.ui.AbsoluteAlignment
65 import androidx.compose.ui.Alignment
66 import androidx.compose.ui.Modifier
67 import androidx.compose.ui.OpenComposeView
68 import androidx.compose.ui.background
69 import androidx.compose.ui.composed
70 import androidx.compose.ui.draw.clipToBounds
71 import androidx.compose.ui.draw.scale
72 import androidx.compose.ui.findAndroidComposeView
73 import androidx.compose.ui.geometry.Offset
74 import androidx.compose.ui.gesture.PointerCoords
75 import androidx.compose.ui.gesture.PointerProperties
76 import androidx.compose.ui.graphics.Color
77 import androidx.compose.ui.graphics.graphicsLayer
78 import androidx.compose.ui.layout.Layout
79 import androidx.compose.ui.layout.LayoutCoordinates
80 import androidx.compose.ui.layout.findRootCoordinates
81 import androidx.compose.ui.layout.layout
82 import androidx.compose.ui.layout.onGloballyPositioned
83 import androidx.compose.ui.layout.onPlaced
84 import androidx.compose.ui.platform.AndroidComposeView
85 import androidx.compose.ui.platform.ComposeView
86 import androidx.compose.ui.platform.LocalDensity
87 import androidx.compose.ui.platform.LocalViewConfiguration
88 import androidx.compose.ui.unit.Dp
89 import androidx.compose.ui.unit.IntSize
90 import androidx.compose.ui.unit.dp
91 import androidx.compose.ui.util.fastAll
92 import androidx.compose.ui.util.fastForEach
93 import androidx.compose.ui.viewinterop.AndroidView
94 import androidx.compose.ui.zIndex
95 import androidx.test.ext.junit.runners.AndroidJUnit4
96 import androidx.test.filters.SdkSuppress
97 import androidx.test.filters.SmallTest
98 import androidx.testutils.waitForFutureFrame
99 import com.google.common.truth.Truth.assertThat
100 import java.util.concurrent.CountDownLatch
101 import java.util.concurrent.TimeUnit
102 import kotlin.math.ceil
103 import kotlinx.coroutines.CoroutineScope
104 import kotlinx.coroutines.delay
105 import kotlinx.coroutines.launch
106 import org.junit.Assert.assertEquals
107 import org.junit.Assert.assertFalse
108 import org.junit.Assert.assertTrue
109 import org.junit.Before
110 import org.junit.Rule
111 import org.junit.Test
112 import org.junit.runner.RunWith
113 import org.mockito.kotlin.any
114 import org.mockito.kotlin.never
115 import org.mockito.kotlin.spy
116 import org.mockito.kotlin.verify
117 
118 @SmallTest
119 @RunWith(AndroidJUnit4::class)
120 class AndroidPointerInputTest {
121     @Suppress("DEPRECATION")
122     @get:Rule
123     val rule = androidx.test.rule.ActivityTestRule(AndroidPointerInputTestActivity::class.java)
124 
125     private lateinit var container: OpenComposeView
126 
127     private fun roundUpDpToNearestHundred(dpValue: Dp): Dp {
128         val roundedValue = ceil(dpValue.value / 100f).toInt() * 100
129         return roundedValue.dp
130     }
131 
132     @Before
133     fun setup() {
134         val activity = rule.activity
135         container = spy(OpenComposeView(activity))
136 
137         rule.runOnUiThread {
138             activity.setContentView(
139                 container,
140                 ViewGroup.LayoutParams(
141                     ViewGroup.LayoutParams.WRAP_CONTENT,
142                     ViewGroup.LayoutParams.WRAP_CONTENT
143                 )
144             )
145         }
146     }
147 
148     @Test
149     fun dispatchTouchEvent_invalidCoordinates() {
150         countDown { latch ->
151             rule.runOnUiThread {
152                 container.setContent {
153                     FillLayout(
154                         Modifier.consumeMovementGestureFilter().onGloballyPositioned {
155                             latch.countDown()
156                         }
157                     )
158                 }
159             }
160         }
161 
162         rule.runOnUiThread {
163             val motionEvent =
164                 MotionEvent(
165                     0,
166                     ACTION_DOWN,
167                     1,
168                     0,
169                     arrayOf(PointerProperties(0)),
170                     arrayOf(PointerCoords(Float.NaN, Float.NaN))
171                 )
172 
173             val androidComposeView = findAndroidComposeView(container)!!
174             // Act
175             val actual = androidComposeView.dispatchTouchEvent(motionEvent)
176 
177             // Assert
178             assertThat(actual).isFalse()
179         }
180     }
181 
182     @Test
183     fun dispatchTouchEvent_infiniteCoordinates() {
184         countDown { latch ->
185             rule.runOnUiThread {
186                 container.setContent {
187                     FillLayout(
188                         Modifier.consumeMovementGestureFilter().onGloballyPositioned {
189                             latch.countDown()
190                         }
191                     )
192                 }
193             }
194         }
195 
196         rule.runOnUiThread {
197             val motionEvent =
198                 MotionEvent(
199                     0,
200                     ACTION_DOWN,
201                     1,
202                     0,
203                     arrayOf(PointerProperties(0)),
204                     arrayOf(PointerCoords(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY))
205                 )
206 
207             val androidComposeView = findAndroidComposeView(container)!!
208             // Act
209             val actual = androidComposeView.dispatchTouchEvent(motionEvent)
210 
211             // Assert
212             assertThat(actual).isFalse()
213         }
214     }
215 
216     @Test
217     @SdkSuppress(maxSdkVersion = 34) // b/384972397: Failing on SDK 35
218     fun dispatchTouchEvent_noPointerInputModifiers_returnsFalse() {
219 
220         // Arrange
221 
222         countDown { latch ->
223             rule.runOnUiThread {
224                 container.setContent {
225                     FillLayout(Modifier.onGloballyPositioned { latch.countDown() })
226                 }
227             }
228         }
229 
230         rule.runOnUiThread {
231             val motionEvent =
232                 MotionEvent(
233                     0,
234                     ACTION_DOWN,
235                     1,
236                     0,
237                     arrayOf(PointerProperties(0)),
238                     arrayOf(PointerCoords(0f, 0f))
239                 )
240 
241             // Act
242             val actual = findRootView(container).dispatchTouchEvent(motionEvent)
243 
244             // Assert
245             assertThat(actual).isFalse()
246         }
247     }
248 
249     /**
250      * Recreates dispatch of non-system created cancellation [MotionEvent] (that is, developer
251      * created) while system is already handling multiple [MotionEvent]s. Due to the platform not
252      * allowing reentrancy while handling [MotionEvent]s, the cancellation event will be ignored.
253      */
254     @Test
255     fun dispatchTouchEvents_eventCancelledDuringProcessing_doesNotCancel() {
256         // Arrange
257         var topBoxInnerCoordinates: LayoutCoordinates? = null
258         var bottomBoxInnerCoordinates: LayoutCoordinates? = null
259 
260         val latch = CountDownLatch(2)
261 
262         val pointerEventsLog = mutableListOf<PointerEvent>()
263 
264         rule.runOnUiThread {
265             container.setContent {
266                 Box(modifier = Modifier.fillMaxSize()) {
267                     // Top Box
268                     Box(
269                         modifier =
270                             Modifier.size(50.dp)
271                                 .align(AbsoluteAlignment.TopLeft)
272                                 .pointerInput(Unit) {
273                                     awaitPointerEventScope {
274                                         while (true) {
275                                             val event = awaitPointerEvent()
276                                             event.changes.forEach { it.consume() }
277                                             pointerEventsLog += event
278 
279                                             // Actual dispatch of non-system created cancellation
280                                             // [MotionEvent] while other [MotionEvent]s are being
281                                             // handled.
282                                             if (event.type == PointerEventType.Move) {
283                                                 dispatchTouchEvent(
284                                                     ACTION_CANCEL,
285                                                     topBoxInnerCoordinates!!
286                                                 )
287                                             }
288                                         }
289                                     }
290                                 }
291                                 .onGloballyPositioned {
292                                     topBoxInnerCoordinates = it
293                                     latch.countDown()
294                                 }
295                     )
296 
297                     // Bottom Box
298                     Box(
299                         modifier =
300                             Modifier.size(60.dp)
301                                 .align(AbsoluteAlignment.BottomRight)
302                                 .pointerInput(Unit) {
303                                     awaitPointerEventScope {
304                                         while (true) {
305                                             val event = awaitPointerEvent()
306                                             event.changes.forEach { it.consume() }
307                                             pointerEventsLog += event
308                                         }
309                                     }
310                                 }
311                                 .onGloballyPositioned {
312                                     bottomBoxInnerCoordinates = it
313                                     latch.countDown()
314                                 }
315                     )
316                 }
317             }
318         }
319 
320         assertTrue(latch.await(1, TimeUnit.SECONDS))
321 
322         rule.runOnUiThread {
323             // Arrange continued
324             val root = topBoxInnerCoordinates!!.findRootCoordinates()
325             val topBoxOffset = root.localPositionOf(topBoxInnerCoordinates!!, Offset.Zero)
326             val bottomBoxOffset = root.localPositionOf(bottomBoxInnerCoordinates!!, Offset.Zero)
327 
328             val topBoxFingerPointerPropertiesId = 0
329             val bottomBoxFingerPointerPropertiesId = 1
330 
331             val topBoxPointerProperties =
332                 PointerProperties(topBoxFingerPointerPropertiesId).also {
333                     it.toolType = MotionEvent.TOOL_TYPE_FINGER
334                 }
335             val bottomBoxPointerProperties =
336                 PointerProperties(bottomBoxFingerPointerPropertiesId).also {
337                     it.toolType = MotionEvent.TOOL_TYPE_FINGER
338                 }
339 
340             var eventStartTime = 0
341 
342             val downTopBoxEvent =
343                 MotionEvent(
344                     eventStartTime,
345                     action = ACTION_DOWN,
346                     numPointers = 1,
347                     actionIndex = 0,
348                     pointerProperties = arrayOf(topBoxPointerProperties),
349                     pointerCoords = arrayOf(PointerCoords(topBoxOffset.x, topBoxOffset.y))
350                 )
351 
352             eventStartTime += 500
353             val downBottomBoxEvent =
354                 MotionEvent(
355                     eventStartTime,
356                     action = ACTION_POINTER_DOWN,
357                     numPointers = 2,
358                     actionIndex = 1,
359                     pointerProperties =
360                         arrayOf(topBoxPointerProperties, bottomBoxPointerProperties),
361                     pointerCoords =
362                         arrayOf(
363                             PointerCoords(topBoxOffset.x, topBoxOffset.y),
364                             PointerCoords(bottomBoxOffset.x, bottomBoxOffset.y)
365                         )
366                 )
367 
368             eventStartTime += 500
369             val moveTopBoxEvent =
370                 MotionEvent(
371                     eventStartTime,
372                     action = ACTION_MOVE,
373                     numPointers = 2,
374                     actionIndex = 0,
375                     pointerProperties =
376                         arrayOf(topBoxPointerProperties, bottomBoxPointerProperties),
377                     pointerCoords =
378                         arrayOf(
379                             PointerCoords(topBoxOffset.x + 10, topBoxOffset.y),
380                             PointerCoords(bottomBoxOffset.x + 10, bottomBoxOffset.y)
381                         )
382                 )
383 
384             eventStartTime += 500
385             val moveBottomBoxEvent =
386                 MotionEvent(
387                     eventStartTime,
388                     action = ACTION_MOVE,
389                     numPointers = 2,
390                     actionIndex = 1,
391                     pointerProperties =
392                         arrayOf(topBoxPointerProperties, bottomBoxPointerProperties),
393                     pointerCoords =
394                         arrayOf(
395                             PointerCoords(topBoxOffset.x + 10, topBoxOffset.y),
396                             PointerCoords(bottomBoxOffset.x + 10, bottomBoxOffset.y)
397                         )
398                 )
399 
400             eventStartTime += 500
401             val upTopBoxEvent =
402                 MotionEvent(
403                     eventStartTime,
404                     action = ACTION_POINTER_UP,
405                     numPointers = 2,
406                     actionIndex = 0,
407                     pointerProperties =
408                         arrayOf(topBoxPointerProperties, bottomBoxPointerProperties),
409                     pointerCoords =
410                         arrayOf(
411                             PointerCoords(topBoxOffset.x + 10, topBoxOffset.y),
412                             PointerCoords(bottomBoxOffset.x + 10, bottomBoxOffset.y)
413                         )
414                 )
415 
416             eventStartTime += 500
417             val upBottomBoxEvent =
418                 MotionEvent(
419                     eventStartTime,
420                     action = ACTION_UP,
421                     numPointers = 1,
422                     actionIndex = 0,
423                     pointerProperties = arrayOf(bottomBoxPointerProperties),
424                     pointerCoords =
425                         arrayOf(PointerCoords(bottomBoxOffset.x + 10, bottomBoxOffset.y))
426                 )
427 
428             // Act
429             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
430 
431             androidComposeView.dispatchTouchEvent(downTopBoxEvent)
432             androidComposeView.dispatchTouchEvent(downBottomBoxEvent)
433             androidComposeView.dispatchTouchEvent(moveTopBoxEvent)
434             androidComposeView.dispatchTouchEvent(moveBottomBoxEvent)
435             androidComposeView.dispatchTouchEvent(upTopBoxEvent)
436             androidComposeView.dispatchTouchEvent(upBottomBoxEvent)
437 
438             // Assert
439             assertThat(pointerEventsLog).hasSize(8)
440 
441             for (pointerEvent in pointerEventsLog) {
442                 assertThat(pointerEvent.internalPointerEvent).isNotNull()
443             }
444 
445             assertThat(pointerEventsLog[0].type).isEqualTo(PointerEventType.Press)
446             assertThat(pointerEventsLog[1].type).isEqualTo(PointerEventType.Press)
447             assertThat(pointerEventsLog[2].type).isEqualTo(PointerEventType.Press)
448 
449             assertThat(pointerEventsLog[3].type).isEqualTo(PointerEventType.Move)
450             assertThat(pointerEventsLog[4].type).isEqualTo(PointerEventType.Move)
451 
452             assertThat(pointerEventsLog[5].type).isEqualTo(PointerEventType.Release)
453             assertThat(pointerEventsLog[6].type).isEqualTo(PointerEventType.Release)
454             assertThat(pointerEventsLog[7].type).isEqualTo(PointerEventType.Release)
455         }
456     }
457 
458     @Test
459     fun dispatchTouchEvent_pointerInputModifier_returnsTrue() {
460 
461         // Arrange
462 
463         countDown { latch ->
464             rule.runOnUiThread {
465                 container.setContent {
466                     FillLayout(
467                         Modifier.consumeMovementGestureFilter().onGloballyPositioned {
468                             latch.countDown()
469                         }
470                     )
471                 }
472             }
473         }
474 
475         rule.runOnUiThread {
476             val locationInWindow = IntArray(2).also { container.getLocationInWindow(it) }
477 
478             val motionEvent =
479                 MotionEvent(
480                     0,
481                     ACTION_DOWN,
482                     1,
483                     0,
484                     arrayOf(PointerProperties(0)),
485                     arrayOf(
486                         PointerCoords(locationInWindow[0].toFloat(), locationInWindow[1].toFloat())
487                     )
488                 )
489 
490             // Act
491             val actual = findRootView(container).dispatchTouchEvent(motionEvent)
492 
493             // Assert
494             assertThat(actual).isTrue()
495         }
496     }
497 
498     @Test
499     fun dispatchTouchEvent_movementNotConsumed_requestDisallowInterceptTouchEventNotCalled() {
500         dispatchTouchEvent_movementConsumptionInCompose(
501             consumeMovement = false,
502             callsRequestDisallowInterceptTouchEvent = false
503         )
504     }
505 
506     @Test
507     fun dispatchTouchEvent_movementConsumed_requestDisallowInterceptTouchEventCalled() {
508         dispatchTouchEvent_movementConsumptionInCompose(
509             consumeMovement = true,
510             callsRequestDisallowInterceptTouchEvent = true
511         )
512     }
513 
514     @Test
515     fun dispatchTouchEvent_notMeasuredLayoutsAreMeasuredFirst() {
516         val size = mutableStateOf(10)
517         val latch = CountDownLatch(1)
518         var consumedDownPosition: Offset? = null
519         rule.runOnUiThread {
520             container.setContent {
521                 Box(Modifier.fillMaxSize().wrapContentSize(align = AbsoluteAlignment.TopLeft)) {
522                     Layout(
523                         {},
524                         Modifier.consumeDownGestureFilter { consumedDownPosition = it }
525                             .onGloballyPositioned { latch.countDown() }
526                     ) { _, _ ->
527                         val sizePx = size.value
528                         layout(sizePx, sizePx) {}
529                     }
530                 }
531             }
532         }
533 
534         assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue()
535 
536         rule.runOnUiThread {
537             // we update size from 10 to 20 pixels
538             size.value = 20
539             // this call will synchronously mark the LayoutNode as needs remeasure
540             Snapshot.sendApplyNotifications()
541             val locationInWindow = IntArray(2).also { container.getLocationInWindow(it) }
542 
543             val motionEvent =
544                 MotionEvent(
545                     0,
546                     ACTION_DOWN,
547                     1,
548                     0,
549                     arrayOf(PointerProperties(0)),
550                     arrayOf(PointerCoords(locationInWindow[0] + 15f, locationInWindow[1] + 15f))
551                 )
552 
553             // we expect it to first remeasure and only then process
554             findRootView(container).dispatchTouchEvent(motionEvent)
555 
556             assertThat(consumedDownPosition).isEqualTo(Offset(15f, 15f))
557         }
558     }
559 
560     @Test
561     fun dispatchTouchEvent_throughLayersOfAndroidAndCompose_hitsChildWithCorrectCoords() {
562 
563         // Arrange
564 
565         val context = rule.activity
566 
567         val log = mutableListOf<List<PointerInputChange>>()
568 
569         countDown { latch ->
570             rule.runOnUiThread {
571                 container.setContent {
572                     AndroidWithCompose(context, 1) {
573                         AndroidWithCompose(context, 10) {
574                             AndroidWithCompose(context, 100) {
575                                 Layout(
576                                     {},
577                                     Modifier.logEventsGestureFilter(log).onGloballyPositioned {
578                                         latch.countDown()
579                                     }
580                                 ) { _, _ ->
581                                     layout(5, 5) {}
582                                 }
583                             }
584                         }
585                     }
586                 }
587             }
588         }
589 
590         rule.runOnUiThread {
591             val locationInWindow = IntArray(2).also { container.getLocationInWindow(it) }
592 
593             val motionEvent =
594                 MotionEvent(
595                     0,
596                     ACTION_DOWN,
597                     1,
598                     0,
599                     arrayOf(PointerProperties(0)),
600                     arrayOf(
601                         PointerCoords(
602                             locationInWindow[0].toFloat() + 1 + 10 + 100,
603                             locationInWindow[1].toFloat() + 1 + 10 + 100
604                         )
605                     )
606                 )
607 
608             // Act
609             findRootView(container).dispatchTouchEvent(motionEvent)
610 
611             // Assert
612             assertThat(log).hasSize(1)
613             assertThat(log[0]).hasSize(1)
614             assertThat(log[0][0].position).isEqualTo(Offset(0f, 0f))
615         }
616     }
617 
618     private fun dispatchTouchEvent_movementConsumptionInCompose(
619         consumeMovement: Boolean,
620         callsRequestDisallowInterceptTouchEvent: Boolean
621     ) {
622 
623         // Arrange
624 
625         countDown { latch ->
626             rule.runOnUiThread {
627                 container.setContent {
628                     FillLayout(
629                         Modifier.consumeMovementGestureFilter(consumeMovement)
630                             .onGloballyPositioned { latch.countDown() }
631                     )
632                 }
633             }
634         }
635 
636         rule.runOnUiThread {
637             val (x, y) =
638                 IntArray(2).let { array ->
639                     container.getLocationInWindow(array)
640                     array.map { item -> item.toFloat() }
641                 }
642 
643             val down =
644                 MotionEvent(
645                     0,
646                     ACTION_DOWN,
647                     1,
648                     0,
649                     arrayOf(PointerProperties(0)),
650                     arrayOf(PointerCoords(x, y))
651                 )
652 
653             val move =
654                 MotionEvent(
655                     0,
656                     ACTION_MOVE,
657                     1,
658                     0,
659                     arrayOf(PointerProperties(0)),
660                     arrayOf(PointerCoords(x + 1, y))
661                 )
662 
663             findRootView(container).dispatchTouchEvent(down)
664 
665             // Act
666             findRootView(container).dispatchTouchEvent(move)
667 
668             // Assert
669             if (callsRequestDisallowInterceptTouchEvent) {
670                 verify(container).requestDisallowInterceptTouchEvent(true)
671             } else {
672                 verify(container, never()).requestDisallowInterceptTouchEvent(any())
673             }
674         }
675     }
676 
677     /**
678      * This test verifies that if the AndroidComposeView is offset directly by a call to
679      * "offsetTopAndBottom(int)", that pointer locations are correct when dispatched down to a child
680      * PointerInputModifier.
681      */
682     @Test
683     fun dispatchTouchEvent_androidComposeViewOffset_positionIsCorrect() {
684 
685         // Arrange
686 
687         val offset = 50
688         val log = mutableListOf<List<PointerInputChange>>()
689 
690         countDown { latch ->
691             rule.runOnUiThread {
692                 container.setContent {
693                     FillLayout(
694                         Modifier.logEventsGestureFilter(log).onGloballyPositioned {
695                             latch.countDown()
696                         }
697                     )
698                 }
699             }
700         }
701 
702         rule.runOnUiThread {
703             // Get the current location in window.
704             val locationInWindow = IntArray(2).also { container.getLocationInWindow(it) }
705 
706             // Offset the androidComposeView.
707             container.offsetTopAndBottom(offset)
708 
709             // Create a motion event that is also offset.
710             val motionEvent =
711                 MotionEvent(
712                     0,
713                     ACTION_DOWN,
714                     1,
715                     0,
716                     arrayOf(PointerProperties(0)),
717                     arrayOf(
718                         PointerCoords(
719                             locationInWindow[0].toFloat(),
720                             locationInWindow[1].toFloat() + offset
721                         )
722                     )
723                 )
724 
725             // Act
726             findRootView(container).dispatchTouchEvent(motionEvent)
727 
728             // Assert
729             assertThat(log).hasSize(1)
730             assertThat(log[0]).hasSize(1)
731             assertThat(log[0][0].position).isEqualTo(Offset(0f, 0f))
732         }
733     }
734 
735     /*
736      * Tests that a long press is NOT triggered when an up event (following a down event) isn't
737      * executed right away because the UI thread is delayed past the long press timeout.
738      *
739      * Note: This test is a bit complicated because it needs to properly execute events in order
740      * using multiple coroutine delay()s and Thread.sleep() in the main thread.
741      *
742      * Expected behavior: When the UI thread wakes up, the up event should be triggered before the
743      * second delay() in the withTimeout() (which foundation's long press uses). Thus, the tap
744      * triggers and NOT the long press.
745      *
746      * Actual steps this test uses to recreate this scenario:
747      *   1. Down event is triggered
748      *   2. An up event is scheduled to be triggered BEFORE the timeout for a long press (uses
749      *       a coroutine sleep() that is less than the long press timeout).
750      *   3. The UI thread sleeps before the sleep() awakens to fire the up event (the sleep time is
751      *       LONGER than the long press timeout).
752      *   4. The UI thread wakes up, executes the first sleep() for the long press timeout
753      *       (in withTimeout() implementation [`SuspendingPointerInputModifierNodeImpl`])
754      *   5. The up event is fired (sleep() for test coroutine finishes).
755      *   6. Tap is triggered (that is, long press is NOT triggered because the second sleep() is
756      *       NOT executed in withTimeout()).
757      */
758     @Test
759     fun detectTapGestures_blockedMainThread() {
760         var didLongPress = false
761         var didTap = false
762 
763         val positionedLatch = CountDownLatch(1)
764         var pressLatch = CountDownLatch(1)
765         var clickOrLongPressLatch = CountDownLatch(1)
766 
767         lateinit var coroutineScope: CoroutineScope
768 
769         val locationInWindow = IntArray(2)
770 
771         // Less than long press timeout
772         val touchUpDelay = 100
773         // 400L
774         val longPressTimeout = android.view.ViewConfiguration.getLongPressTimeout()
775         // Goes past long press timeout (above)
776         val sleepTime = longPressTimeout + 100L
777         // matches first delay time in [PointerEventHandlerCoroutine.withTimeout()]
778         val withTimeoutDelay = longPressTimeout - WITH_TIMEOUT_MICRO_DELAY_MILLIS
779         var upEvent: MotionEvent? = null
780 
781         rule.runOnUiThread {
782             container.setContent {
783                 coroutineScope = rememberCoroutineScope()
784 
785                 FillLayout(
786                     Modifier.pointerInput(Unit) {
787                             detectTapGestures(
788                                 onLongPress = {
789                                     didLongPress = true
790                                     clickOrLongPressLatch.countDown()
791                                 },
792                                 onTap = {
793                                     didTap = true
794                                     clickOrLongPressLatch.countDown()
795                                 },
796                                 onPress = {
797                                     // AwaitPointerEventScope.waitForLongPress() uses
798                                     // PointerEventHandlerCoroutine.withTimeout() as part of
799                                     // the timeout logic to see if a long press has occurred.
800                                     //
801                                     // Within PointerEventHandlerCoroutine.withTimeout(), there
802                                     // is a coroutine with two delay() calls and we are
803                                     // specifically testing that an up event that is put to
804                                     // sleep (but within the timeout time), does not trigger a
805                                     // long press when it comes in between those delay() calls.
806                                     //
807                                     // To do that, we want to get the timing of this coroutine
808                                     // as close to timeout as possible. That is, executing the
809                                     // up event (after the delay below) right between those
810                                     // delays to avoid the test being flaky.
811                                     coroutineScope.launch {
812                                         // Matches first delay used with withTimeout() for long
813                                         // press.
814                                         delay(withTimeoutDelay)
815                                         findRootView(container).dispatchTouchEvent(upEvent!!)
816                                     }
817                                     pressLatch.countDown()
818                                 }
819                             )
820                         }
821                         .onGloballyPositioned { positionedLatch.countDown() }
822                 )
823             }
824         }
825 
826         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
827         container.getLocationInWindow(locationInWindow)
828 
829         repeat(5) { iteration ->
830             rule.runOnUiThread {
831                 val downEvent =
832                     createPointerEventAt(
833                         iteration * sleepTime.toInt(),
834                         ACTION_DOWN,
835                         locationInWindow
836                     )
837 
838                 upEvent =
839                     createPointerEventAt(
840                         touchUpDelay + iteration * sleepTime.toInt(),
841                         ACTION_UP,
842                         locationInWindow
843                     )
844                 findRootView(container).dispatchTouchEvent(downEvent)
845             }
846 
847             assertTrue(pressLatch.await(1, TimeUnit.SECONDS))
848 
849             // Blocks the UI thread from now until past the long-press
850             // timeout. This tests that even in pathological situations,
851             // the upEvent is still processed before the long-press
852             // timeout.
853             rule.runOnUiThread { Thread.sleep(sleepTime) }
854 
855             assertTrue(clickOrLongPressLatch.await(1, TimeUnit.SECONDS))
856 
857             assertFalse(didLongPress)
858             assertTrue(didTap)
859 
860             didTap = false
861             clickOrLongPressLatch = CountDownLatch(1)
862             pressLatch = CountDownLatch(1)
863         }
864     }
865 
866     /**
867      * When a modifier is added, it should work, even when it takes the position of a previous
868      * modifier.
869      */
870     @Test
871     fun recomposeWithNewModifier() {
872         var tap2Enabled by mutableStateOf(false)
873         var tapLatch = CountDownLatch(1)
874         val tapLatch2 = CountDownLatch(1)
875         var positionedLatch = CountDownLatch(1)
876 
877         rule.runOnUiThread {
878             container.setContent {
879                 FillLayout(
880                     Modifier.pointerInput(Unit) { detectTapGestures { tapLatch.countDown() } }
881                         .then(
882                             if (tap2Enabled)
883                                 Modifier.pointerInput(Unit) {
884                                     detectTapGestures { tapLatch2.countDown() }
885                                 }
886                             else Modifier
887                         )
888                         .onGloballyPositioned { positionedLatch.countDown() }
889                 )
890             }
891         }
892 
893         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
894 
895         val locationInWindow = IntArray(2)
896         rule.runOnUiThread {
897             // Get the current location in window.
898             container.getLocationInWindow(locationInWindow)
899 
900             val downEvent = createPointerEventAt(0, ACTION_DOWN, locationInWindow)
901             findRootView(container).dispatchTouchEvent(downEvent)
902         }
903 
904         rule.runOnUiThread {
905             val upEvent = createPointerEventAt(200, ACTION_UP, locationInWindow)
906             findRootView(container).dispatchTouchEvent(upEvent)
907         }
908 
909         assertTrue(tapLatch.await(1, TimeUnit.SECONDS))
910         tapLatch = CountDownLatch(1)
911 
912         positionedLatch = CountDownLatch(1)
913         tap2Enabled = true
914         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
915 
916         rule.runOnUiThread {
917             val downEvent = createPointerEventAt(1000, ACTION_DOWN, locationInWindow)
918             findRootView(container).dispatchTouchEvent(downEvent)
919         }
920         // Need to wait for long press timeout (at least)
921         rule.runOnUiThread {
922             val upEvent = createPointerEventAt(1030, ACTION_UP, locationInWindow)
923             findRootView(container).dispatchTouchEvent(upEvent)
924         }
925         assertTrue(tapLatch2.await(1, TimeUnit.SECONDS))
926 
927         positionedLatch = CountDownLatch(1)
928         tap2Enabled = false
929         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
930 
931         rule.runOnUiThread {
932             val downEvent = createPointerEventAt(2000, ACTION_DOWN, locationInWindow)
933             findRootView(container).dispatchTouchEvent(downEvent)
934         }
935         rule.runOnUiThread {
936             val upEvent = createPointerEventAt(2200, ACTION_UP, locationInWindow)
937             findRootView(container).dispatchTouchEvent(upEvent)
938         }
939         assertTrue(tapLatch.await(1, TimeUnit.SECONDS))
940     }
941 
942     /**
943      * There are times that getLocationOnScreen() returns (0, 0). Touch input should still arrive at
944      * the correct place even if getLocationOnScreen() gives a different result than the rawX, rawY
945      * indicate.
946      */
947     @Test
948     fun badGetLocationOnScreen() {
949         val tapLatch = CountDownLatch(1)
950         val layoutLatch = CountDownLatch(1)
951         rule.runOnUiThread {
952             container.setContent {
953                 with(LocalDensity.current) {
954                     Box(
955                         Modifier.size(250.toDp()).layout { measurable, constraints ->
956                             val p = measurable.measure(constraints)
957                             layout(p.width, p.height) {
958                                 p.place(0, 0)
959                                 layoutLatch.countDown()
960                             }
961                         }
962                     ) {
963                         Box(
964                             Modifier.align(AbsoluteAlignment.TopLeft)
965                                 .pointerInput(Unit) {
966                                     awaitPointerEventScope {
967                                         awaitFirstDown()
968                                         tapLatch.countDown()
969                                     }
970                                 }
971                                 .size(10.toDp())
972                         )
973                     }
974                 }
975             }
976         }
977         assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
978         rule.runOnUiThread {}
979 
980         val down = createPointerEventAt(0, ACTION_DOWN, intArrayOf(105, 205))
981         down.offsetLocation(-100f, -200f)
982         val composeView = findAndroidComposeView(container) as AndroidComposeView
983         composeView.dispatchTouchEvent(down)
984 
985         assertTrue(tapLatch.await(1, TimeUnit.SECONDS))
986     }
987 
988     /**
989      * When a scale(0, 0) is used, there is no valid inverse matrix. A touch should not reach an
990      * item that is scaled to 0.
991      */
992     @Test
993     fun badInverseMatrix() {
994         val tapLatch = CountDownLatch(1)
995         val layoutLatch = CountDownLatch(1)
996         var insideTap = 0
997         rule.runOnUiThread {
998             container.setContent {
999                 with(LocalDensity.current) {
1000                     Box(
1001                         Modifier.layout { measurable, constraints ->
1002                                 val p = measurable.measure(constraints)
1003                                 layout(p.width, p.height) {
1004                                     layoutLatch.countDown()
1005                                     p.place(0, 0)
1006                                 }
1007                             }
1008                             .pointerInput(Unit) {
1009                                 awaitPointerEventScope {
1010                                     awaitFirstDown()
1011                                     tapLatch.countDown()
1012                                 }
1013                             }
1014                             .requiredSize(10.toDp())
1015                             .scale(0f, 0f)
1016                             .pointerInput(Unit) {
1017                                 awaitPointerEventScope {
1018                                     awaitFirstDown()
1019                                     insideTap++
1020                                 }
1021                             }
1022                             .requiredSize(10.toDp())
1023                     )
1024                 }
1025             }
1026         }
1027         assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
1028         rule.runOnUiThread {}
1029 
1030         val down = createPointerEventAt(0, ACTION_DOWN, intArrayOf(5, 5))
1031         val composeView = findAndroidComposeView(container) as AndroidComposeView
1032         composeView.dispatchTouchEvent(down)
1033 
1034         assertTrue(tapLatch.await(1, TimeUnit.SECONDS))
1035         rule.runOnUiThread { assertEquals(0, insideTap) }
1036     }
1037 
1038     @Test
1039     fun dispatchNotAttached() {
1040         val tapLatch = CountDownLatch(1)
1041         val layoutLatch = CountDownLatch(1)
1042         rule.runOnUiThread {
1043             container.setContent {
1044                 with(LocalDensity.current) {
1045                     Box(
1046                         Modifier.onPlaced { layoutLatch.countDown() }
1047                             .pointerInput(Unit) {
1048                                 awaitPointerEventScope {
1049                                     awaitFirstDown()
1050                                     tapLatch.countDown()
1051                                 }
1052                             }
1053                             .requiredSize(10.toDp())
1054                     )
1055                 }
1056             }
1057         }
1058         assertTrue(layoutLatch.await(1, TimeUnit.SECONDS))
1059 
1060         val composeView = findAndroidComposeView(container) as AndroidComposeView
1061         rule.runOnUiThread {
1062             container.removeAllViews()
1063             val down = createPointerEventAt(0, ACTION_DOWN, intArrayOf(5, 5))
1064             assertFalse(composeView.dispatchTouchEvent(down))
1065         }
1066     }
1067 
1068     private fun assertHoverEvent(
1069         event: PointerEvent,
1070         isEnter: Boolean = false,
1071         isExit: Boolean = false
1072     ) {
1073         assertThat(event.changes).hasSize(1)
1074         val change = event.changes[0]
1075         assertThat(change.pressed).isFalse()
1076         assertThat(change.previousPressed).isFalse()
1077         val expectedHoverType =
1078             when {
1079                 isEnter -> PointerEventType.Enter
1080                 isExit -> PointerEventType.Exit
1081                 else -> PointerEventType.Move
1082             }
1083         assertThat(event.type).isEqualTo(expectedHoverType)
1084     }
1085 
1086     private fun assertScrollEvent(event: PointerEvent, scrollExpected: Offset) {
1087         assertThat(event.changes).hasSize(1)
1088         val change = event.changes[0]
1089         assertThat(change.pressed).isFalse()
1090         assertThat(event.type).isEqualTo(PointerEventType.Scroll)
1091         // we agreed to reverse Y in android to be in line with other platforms
1092         assertThat(change.scrollDelta).isEqualTo(scrollExpected.copy(y = scrollExpected.y * -1))
1093     }
1094 
1095     private fun dispatchMouseEvent(
1096         action: Int,
1097         layoutCoordinates: LayoutCoordinates,
1098         offset: Offset = Offset.Zero,
1099         scrollDelta: Offset = Offset.Zero,
1100         eventTime: Int = 0
1101     ) {
1102         rule.runOnUiThread {
1103             val root = layoutCoordinates.findRootCoordinates()
1104             val pos = root.localPositionOf(layoutCoordinates, offset)
1105             val event =
1106                 MotionEvent(
1107                     eventTime,
1108                     action,
1109                     1,
1110                     0,
1111                     arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_MOUSE }),
1112                     arrayOf(PointerCoords(pos.x, pos.y, scrollDelta.x, scrollDelta.y))
1113                 )
1114 
1115             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1116             when (action) {
1117                 ACTION_HOVER_ENTER,
1118                 ACTION_HOVER_MOVE,
1119                 ACTION_HOVER_EXIT -> androidComposeView.dispatchHoverEvent(event)
1120                 ACTION_SCROLL -> androidComposeView.dispatchGenericMotionEvent(event)
1121                 else -> androidComposeView.dispatchTouchEvent(event)
1122             }
1123         }
1124     }
1125 
1126     private fun dispatchStylusEvents(
1127         layoutCoordinates: LayoutCoordinates,
1128         offset: Offset,
1129         vararg actions: Int
1130     ) {
1131         rule.runOnUiThread {
1132             val root = layoutCoordinates.findRootCoordinates()
1133             val pos = root.localPositionOf(layoutCoordinates, offset)
1134             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1135 
1136             for (action in actions) {
1137                 val event =
1138                     MotionEvent(
1139                         0,
1140                         action,
1141                         1,
1142                         0,
1143                         arrayOf(
1144                             PointerProperties(0).also { it.toolType = MotionEvent.TOOL_TYPE_STYLUS }
1145                         ),
1146                         arrayOf(PointerCoords(pos.x, pos.y))
1147                     )
1148 
1149                 when (action) {
1150                     ACTION_HOVER_ENTER,
1151                     ACTION_HOVER_MOVE,
1152                     ACTION_HOVER_EXIT -> androidComposeView.dispatchHoverEvent(event)
1153                     else -> androidComposeView.dispatchTouchEvent(event)
1154                 }
1155             }
1156         }
1157     }
1158 
1159     private fun dispatchTouchEvent(
1160         action: Int,
1161         layoutCoordinates: LayoutCoordinates,
1162         offset: Offset = Offset.Zero,
1163         eventTime: Int = 0
1164     ) {
1165         rule.runOnUiThread {
1166             val root = layoutCoordinates.findRootCoordinates()
1167             val pos = root.localPositionOf(layoutCoordinates, offset)
1168             val event =
1169                 MotionEvent(
1170                     eventTime,
1171                     action,
1172                     1,
1173                     0,
1174                     arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_FINGER }),
1175                     arrayOf(PointerCoords(pos.x, pos.y))
1176                 )
1177 
1178             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1179             androidComposeView.dispatchTouchEvent(event)
1180         }
1181     }
1182 
1183     @Test
1184     fun dispatchHoverEnter() {
1185         var layoutCoordinates: LayoutCoordinates? = null
1186         val latch = CountDownLatch(1)
1187         val events = mutableListOf<PointerEvent>()
1188         rule.runOnUiThread {
1189             container.setContent {
1190                 Box(
1191                     Modifier.fillMaxSize()
1192                         .onGloballyPositioned {
1193                             layoutCoordinates = it
1194                             latch.countDown()
1195                         }
1196                         .pointerInput(Unit) {
1197                             awaitPointerEventScope {
1198                                 while (true) {
1199                                     val event = awaitPointerEvent()
1200                                     event.changes[0].consume()
1201                                     events += event
1202                                 }
1203                             }
1204                         }
1205                 )
1206             }
1207         }
1208         assertTrue(latch.await(1, TimeUnit.SECONDS))
1209         dispatchMouseEvent(ACTION_HOVER_ENTER, layoutCoordinates!!)
1210         rule.runOnUiThread {
1211             assertThat(events).hasSize(1)
1212             assertHoverEvent(events[0], isEnter = true)
1213         }
1214     }
1215 
1216     @Test
1217     fun dispatchHoverExit() {
1218         var layoutCoordinates: LayoutCoordinates? = null
1219         val latch = CountDownLatch(1)
1220         val events = mutableListOf<PointerEvent>()
1221         rule.runOnUiThread {
1222             container.setContent {
1223                 Box(
1224                     Modifier.fillMaxSize()
1225                         .onGloballyPositioned {
1226                             layoutCoordinates = it
1227                             latch.countDown()
1228                         }
1229                         .pointerInput(Unit) {
1230                             awaitPointerEventScope {
1231                                 while (true) {
1232                                     val event = awaitPointerEvent()
1233                                     event.changes[0].consume()
1234                                     events += event
1235                                 }
1236                             }
1237                         }
1238                 )
1239             }
1240         }
1241         assertTrue(latch.await(1, TimeUnit.SECONDS))
1242         dispatchMouseEvent(ACTION_HOVER_ENTER, layoutCoordinates!!)
1243         dispatchMouseEvent(ACTION_HOVER_EXIT, layoutCoordinates!!, Offset(-1f, -1f))
1244 
1245         rule.runOnUiThread {
1246             assertThat(events).hasSize(2)
1247             assertHoverEvent(events[0], isEnter = true)
1248             assertHoverEvent(events[1], isExit = true)
1249         }
1250     }
1251 
1252     /*
1253      * Simple test that makes sure a bad ACTION_OUTSIDE MotionEvent doesn't negatively
1254      * impact Compose (b/299074463#comment31). (We actually ignore them in Compose.)
1255      * The event order of MotionEvents:
1256      *   1. Hover enter on box 1
1257      *   2. Hover move into box 2
1258      *   3. Hover exit on box 2
1259      *   4. Outside event on box 3
1260      *   5. Down on box 2
1261      *   6. Up on box 2
1262      */
1263     @Test
1264     fun hoverAndClickMotionEvent_badOutsideMotionEvent_outsideMotionEventIgnored() {
1265         // --> Arrange
1266         var box1LayoutCoordinates: LayoutCoordinates? = null
1267         var box2LayoutCoordinates: LayoutCoordinates? = null
1268         var box3LayoutCoordinates: LayoutCoordinates? = null
1269 
1270         val setUpFinishedLatch = CountDownLatch(4)
1271         // One less than total because outside is not sent to Compose.
1272         val totalEventLatch = CountDownLatch(5)
1273 
1274         // Events for Box 1
1275         var enterBox1 = false
1276         var exitBox1 = false
1277 
1278         // Events for Box 2
1279         var enterBox2 = false
1280         var exitBox2 = false
1281         var pressBox2 = false
1282         var releaseBox2 = false
1283 
1284         // All other events that should never be triggered in this test
1285         var eventsThatShouldNotTrigger = false
1286 
1287         var pointerEvent: PointerEvent? = null
1288 
1289         rule.runOnUiThread {
1290             container.setContent {
1291                 Column(
1292                     Modifier.fillMaxSize()
1293                         .onGloballyPositioned { setUpFinishedLatch.countDown() }
1294                         .pointerInput(Unit) {
1295                             awaitPointerEventScope {
1296                                 while (true) {
1297                                     awaitPointerEvent()
1298                                     totalEventLatch.countDown()
1299                                 }
1300                             }
1301                         }
1302                 ) {
1303                     // Box 1
1304                     Box(
1305                         Modifier.size(50.dp)
1306                             .onGloballyPositioned {
1307                                 box1LayoutCoordinates = it
1308                                 setUpFinishedLatch.countDown()
1309                             }
1310                             .pointerInput(Unit) {
1311                                 awaitPointerEventScope {
1312                                     while (true) {
1313                                         pointerEvent = awaitPointerEvent()
1314 
1315                                         when (pointerEvent!!.type) {
1316                                             PointerEventType.Enter -> {
1317                                                 enterBox1 = true
1318                                             }
1319                                             PointerEventType.Exit -> {
1320                                                 exitBox1 = true
1321                                             }
1322                                             else -> {
1323                                                 eventsThatShouldNotTrigger = true
1324                                             }
1325                                         }
1326                                     }
1327                                 }
1328                             }
1329                     ) {}
1330 
1331                     // Box 2
1332                     Box(
1333                         Modifier.size(50.dp)
1334                             .onGloballyPositioned {
1335                                 box2LayoutCoordinates = it
1336                                 setUpFinishedLatch.countDown()
1337                             }
1338                             .pointerInput(Unit) {
1339                                 awaitPointerEventScope {
1340                                     while (true) {
1341                                         pointerEvent = awaitPointerEvent()
1342 
1343                                         when (pointerEvent!!.type) {
1344                                             PointerEventType.Enter -> {
1345                                                 enterBox2 = true
1346                                             }
1347                                             PointerEventType.Press -> {
1348                                                 pressBox2 = true
1349                                             }
1350                                             PointerEventType.Release -> {
1351                                                 releaseBox2 = true
1352                                             }
1353                                             PointerEventType.Exit -> {
1354                                                 exitBox2 = true
1355                                             }
1356                                             else -> {
1357                                                 eventsThatShouldNotTrigger = true
1358                                             }
1359                                         }
1360                                     }
1361                                 }
1362                             }
1363                     ) {}
1364 
1365                     // Box 3
1366                     Box(
1367                         Modifier.size(50.dp)
1368                             .onGloballyPositioned {
1369                                 box3LayoutCoordinates = it
1370                                 setUpFinishedLatch.countDown()
1371                             }
1372                             .pointerInput(Unit) {
1373                                 awaitPointerEventScope {
1374                                     while (true) {
1375                                         pointerEvent = awaitPointerEvent()
1376                                         eventsThatShouldNotTrigger = true
1377                                     }
1378                                 }
1379                             }
1380                     ) {}
1381                 }
1382             }
1383         }
1384         // Ensure Arrange (setup) step is finished
1385         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
1386 
1387         // --> Act + Assert (interwoven)
1388         // Hover Enter on Box 1
1389         dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
1390         rule.runOnUiThread {
1391             assertThat(enterBox1).isTrue()
1392             assertThat(pointerEvent).isNotNull()
1393             assertThat(eventsThatShouldNotTrigger).isFalse()
1394             assertHoverEvent(pointerEvent!!, isEnter = true)
1395         }
1396 
1397         // Hover Move to Box 2
1398         pointerEvent = null // Reset before each event
1399         dispatchMouseEvent(ACTION_HOVER_MOVE, box2LayoutCoordinates!!)
1400         rule.runOnUiThread {
1401             assertThat(exitBox1).isTrue()
1402             assertThat(enterBox2).isTrue()
1403             assertThat(pointerEvent).isNotNull()
1404             assertThat(eventsThatShouldNotTrigger).isFalse()
1405             assertHoverEvent(pointerEvent!!, isEnter = true)
1406         }
1407 
1408         // Hover Exit on Box 2
1409         pointerEvent = null // Reset before each event
1410         dispatchMouseEvent(ACTION_HOVER_EXIT, box2LayoutCoordinates!!)
1411 
1412         // Hover exit events in Compose are always delayed two frames to ensure Compose does not
1413         // trigger them if they are followed by a press in the next frame. This accounts for that.
1414         rule.waitForFutureFrame(2)
1415 
1416         rule.runOnUiThread {
1417             assertThat(exitBox2).isTrue()
1418             assertThat(pointerEvent).isNotNull()
1419             assertThat(eventsThatShouldNotTrigger).isFalse()
1420         }
1421 
1422         // Outside event with Box 3 coordinates
1423         pointerEvent = null // Reset before each event
1424         dispatchMouseEvent(ACTION_OUTSIDE, box3LayoutCoordinates!!)
1425 
1426         // No Compose event should be triggered (b/299074463#comment31)
1427         rule.runOnUiThread {
1428             assertThat(eventsThatShouldNotTrigger).isFalse()
1429             assertThat(pointerEvent).isNull()
1430         }
1431 
1432         // Press on Box 2
1433         pointerEvent = null // Reset before each event
1434         dispatchMouseEvent(ACTION_DOWN, box2LayoutCoordinates!!)
1435         rule.runOnUiThread {
1436             assertThat(pressBox2).isTrue()
1437             assertThat(eventsThatShouldNotTrigger).isFalse()
1438             assertThat(pointerEvent).isNotNull()
1439         }
1440 
1441         // Release on Box 2
1442         pointerEvent = null // Reset before each event
1443         dispatchMouseEvent(ACTION_UP, box2LayoutCoordinates!!)
1444         rule.runOnUiThread {
1445             assertThat(releaseBox2).isTrue()
1446             assertThat(eventsThatShouldNotTrigger).isFalse()
1447             assertThat(pointerEvent).isNotNull()
1448         }
1449 
1450         assertTrue(totalEventLatch.await(1, TimeUnit.SECONDS))
1451     }
1452 
1453     /*
1454      * Tests that a bad ACTION_HOVER_EXIT MotionEvent is ignored in Compose when it directly
1455      * proceeds an ACTION_SCROLL MotionEvent. This happens in some versions of Android Studio when
1456      * mirroring is used (b/314269723).
1457      *
1458      * The event order of MotionEvents:
1459      *   - Hover enter on box 1
1460      *   - Hover exit on box 1 (bad event)
1461      *   - Scroll on box 1
1462      */
1463     @Test
1464     fun scrollMotionEvent_proceededImmediatelyByHoverExit_shouldNotTriggerHoverExit() {
1465         // --> Arrange
1466         val scrollDelta = Offset(0.35f, 0.65f)
1467         var box1LayoutCoordinates: LayoutCoordinates? = null
1468 
1469         val setUpFinishedLatch = CountDownLatch(4)
1470 
1471         // Events for Box 1
1472         var enterBox1 = false
1473         var scrollBox1 = false
1474 
1475         // All other events that should never be triggered in this test
1476         var eventsThatShouldNotTrigger = false
1477 
1478         var pointerEvent: PointerEvent? = null
1479 
1480         rule.runOnUiThread {
1481             container.setContent {
1482                 Column(
1483                     Modifier.fillMaxSize().onGloballyPositioned { setUpFinishedLatch.countDown() }
1484                 ) {
1485                     // Box 1
1486                     Box(
1487                         Modifier.size(50.dp)
1488                             .onGloballyPositioned {
1489                                 box1LayoutCoordinates = it
1490                                 setUpFinishedLatch.countDown()
1491                             }
1492                             .pointerInput(Unit) {
1493                                 awaitPointerEventScope {
1494                                     while (true) {
1495                                         pointerEvent = awaitPointerEvent()
1496 
1497                                         when (pointerEvent!!.type) {
1498                                             PointerEventType.Enter -> {
1499                                                 enterBox1 = true
1500                                             }
1501                                             PointerEventType.Exit -> {
1502                                                 enterBox1 = false
1503                                             }
1504                                             PointerEventType.Scroll -> {
1505                                                 scrollBox1 = true
1506                                             }
1507                                             else -> {
1508                                                 eventsThatShouldNotTrigger = true
1509                                             }
1510                                         }
1511                                     }
1512                                 }
1513                             }
1514                     ) {}
1515 
1516                     // Box 2
1517                     Box(
1518                         Modifier.size(50.dp)
1519                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
1520                             .pointerInput(Unit) {
1521                                 awaitPointerEventScope {
1522                                     while (true) {
1523                                         pointerEvent = awaitPointerEvent()
1524                                         // Should never do anything with this UI element.
1525                                         eventsThatShouldNotTrigger = true
1526                                     }
1527                                 }
1528                             }
1529                     ) {}
1530 
1531                     // Box 3
1532                     Box(
1533                         Modifier.size(50.dp)
1534                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
1535                             .pointerInput(Unit) {
1536                                 awaitPointerEventScope {
1537                                     while (true) {
1538                                         pointerEvent = awaitPointerEvent()
1539                                         // Should never do anything with this UI element.
1540                                         eventsThatShouldNotTrigger = true
1541                                     }
1542                                 }
1543                             }
1544                     ) {}
1545                 }
1546             }
1547         }
1548         // Ensure Arrange (setup) step is finished
1549         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
1550 
1551         // --> Act + Assert (interwoven)
1552         // Hover Enter on Box 1
1553         dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
1554         rule.runOnUiThread {
1555             assertThat(enterBox1).isTrue()
1556             assertThat(pointerEvent).isNotNull()
1557             assertThat(eventsThatShouldNotTrigger).isFalse()
1558             assertHoverEvent(pointerEvent!!, isEnter = true)
1559         }
1560 
1561         // We do not use dispatchMouseEvent() to dispatch the following two events, because the
1562         // actions need to be executed in immediate succession
1563         rule.runOnUiThread {
1564             val root = box1LayoutCoordinates!!.findRootCoordinates()
1565             val pos = root.localPositionOf(box1LayoutCoordinates!!, Offset.Zero)
1566 
1567             // Bad hover exit event on Box 1
1568             val exitMotionEvent =
1569                 MotionEvent(
1570                     0,
1571                     ACTION_HOVER_EXIT,
1572                     1,
1573                     0,
1574                     arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_MOUSE }),
1575                     arrayOf(PointerCoords(pos.x, pos.y, Offset.Zero.x, Offset.Zero.y))
1576                 )
1577 
1578             // Main scroll event on Box 1
1579             val scrollMotionEvent =
1580                 MotionEvent(
1581                     0,
1582                     ACTION_SCROLL,
1583                     1,
1584                     0,
1585                     arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_MOUSE }),
1586                     arrayOf(PointerCoords(pos.x, pos.y, scrollDelta.x, scrollDelta.y))
1587                 )
1588 
1589             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1590             androidComposeView.dispatchHoverEvent(exitMotionEvent)
1591             androidComposeView.dispatchGenericMotionEvent(scrollMotionEvent)
1592         }
1593 
1594         rule.runOnUiThread {
1595             assertThat(enterBox1).isTrue()
1596             assertThat(scrollBox1).isTrue()
1597             assertThat(pointerEvent).isNotNull()
1598             assertThat(eventsThatShouldNotTrigger).isFalse()
1599         }
1600     }
1601 
1602     /*
1603      * Tests that all valid combinations of MotionEvent.CLASSIFICATION_* are returned from
1604      * Compose's [PointerInput].
1605      * NOTE 1: We do NOT test invalid MotionEvent Classifications, because you can actually pass an
1606      * invalid classification value to [MotionEvent.obtain()] and it is not rejected. Therefore,
1607      * to maintain the same behavior, we just return whatever is set in [MotionEvent].
1608      * NOTE 2: The [MotionEvent.obtain()] that allows you to set classification, is only available
1609      * in U. (Thus, why this test request at least that version.)
1610      */
1611     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
1612     @Test
1613     fun motionEventDispatch_withValidClassification_shouldMatchInPointerEvent() {
1614         // --> Arrange
1615         var boxLayoutCoordinates: LayoutCoordinates? = null
1616         val setUpFinishedLatch = CountDownLatch(1)
1617         var motionEventClassification = MotionEvent.CLASSIFICATION_NONE
1618         var pointerEvent: PointerEvent? = null
1619 
1620         rule.runOnUiThread {
1621             container.setContent {
1622                 Box(
1623                     Modifier.fillMaxSize()
1624                         .onGloballyPositioned {
1625                             setUpFinishedLatch.countDown()
1626                             boxLayoutCoordinates = it
1627                         }
1628                         .pointerInput(Unit) {
1629                             awaitPointerEventScope {
1630                                 while (true) {
1631                                     pointerEvent = awaitPointerEvent()
1632                                 }
1633                             }
1634                         }
1635                 ) {}
1636             }
1637         }
1638 
1639         // Ensure Arrange (setup) step is finished
1640         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
1641 
1642         // Set up values to be used for creation of all MotionEvents.
1643         var position: Offset?
1644         var eventTime = 0
1645         val numPointers = 1
1646         val actionIndex = 0
1647         val pointerProperties =
1648             arrayOf(PointerProperties(0).also { it.toolType = MotionEvent.TOOL_TYPE_FINGER })
1649         var pointerCoords: Array<PointerCoords>? = null
1650         val buttonState = 0
1651 
1652         // --> Act
1653         rule.runOnUiThread {
1654             // Set up pointerCoords to be used for the rest of the events
1655             val root = boxLayoutCoordinates!!.findRootCoordinates()
1656             position = root.localPositionOf(boxLayoutCoordinates!!, Offset.Zero)
1657             pointerCoords =
1658                 arrayOf(PointerCoords(position!!.x, position!!.y, Offset.Zero.x, Offset.Zero.y))
1659 
1660             val downEvent =
1661                 MotionEvent(
1662                     eventTime = eventTime,
1663                     action = ACTION_DOWN,
1664                     numPointers = numPointers,
1665                     actionIndex = actionIndex,
1666                     pointerProperties = pointerProperties,
1667                     pointerCoords = pointerCoords!!,
1668                     buttonState = buttonState,
1669                     classification = motionEventClassification
1670                 )
1671 
1672             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1673             androidComposeView.dispatchTouchEvent(downEvent)
1674         }
1675 
1676         // --> Assert
1677         rule.runOnUiThread {
1678             assertThat(pointerEvent).isNotNull()
1679             // This will be MotionEvent.CLASSIFICATION_NONE (set in the beginning).
1680             assertThat(pointerEvent!!.classification).isEqualTo(motionEventClassification)
1681         }
1682 
1683         eventTime += 500
1684         motionEventClassification = MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
1685 
1686         // --> Act
1687         rule.runOnUiThread {
1688             val upEvent =
1689                 MotionEvent(
1690                     eventTime = eventTime,
1691                     action = ACTION_UP,
1692                     numPointers = numPointers,
1693                     actionIndex = actionIndex,
1694                     pointerProperties = pointerProperties,
1695                     pointerCoords = pointerCoords!!,
1696                     buttonState = buttonState,
1697                     classification = motionEventClassification
1698                 )
1699 
1700             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1701             androidComposeView.dispatchTouchEvent(upEvent)
1702         }
1703 
1704         // --> Assert
1705         rule.runOnUiThread {
1706             assertThat(pointerEvent).isNotNull()
1707             assertThat(pointerEvent!!.classification).isEqualTo(motionEventClassification)
1708         }
1709 
1710         eventTime += 500
1711         motionEventClassification = MotionEvent.CLASSIFICATION_DEEP_PRESS
1712 
1713         // --> Act
1714         rule.runOnUiThread {
1715             val downEvent =
1716                 MotionEvent(
1717                     eventTime = eventTime,
1718                     action = ACTION_DOWN,
1719                     numPointers = numPointers,
1720                     actionIndex = actionIndex,
1721                     pointerProperties = pointerProperties,
1722                     pointerCoords = pointerCoords!!,
1723                     buttonState = buttonState,
1724                     classification = motionEventClassification
1725                 )
1726 
1727             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1728             androidComposeView.dispatchTouchEvent(downEvent)
1729         }
1730 
1731         // --> Assert
1732         rule.runOnUiThread {
1733             assertThat(pointerEvent).isNotNull()
1734             assertThat(pointerEvent!!.classification).isEqualTo(motionEventClassification)
1735         }
1736 
1737         eventTime += 500
1738         motionEventClassification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
1739 
1740         // --> Act
1741         rule.runOnUiThread {
1742             val upEvent =
1743                 MotionEvent(
1744                     eventTime = eventTime,
1745                     action = ACTION_UP,
1746                     numPointers = numPointers,
1747                     actionIndex = actionIndex,
1748                     pointerProperties = pointerProperties,
1749                     pointerCoords = pointerCoords!!,
1750                     buttonState = buttonState,
1751                     classification = motionEventClassification
1752                 )
1753 
1754             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1755             androidComposeView.dispatchTouchEvent(upEvent)
1756         }
1757 
1758         // --> Assert
1759         rule.runOnUiThread {
1760             assertThat(pointerEvent).isNotNull()
1761             assertThat(pointerEvent!!.classification).isEqualTo(motionEventClassification)
1762         }
1763 
1764         eventTime += 500
1765         motionEventClassification = MotionEvent.CLASSIFICATION_PINCH
1766 
1767         // --> Act
1768         rule.runOnUiThread {
1769             val downEvent =
1770                 MotionEvent(
1771                     eventTime = eventTime,
1772                     action = ACTION_DOWN,
1773                     numPointers = numPointers,
1774                     actionIndex = actionIndex,
1775                     pointerProperties = pointerProperties,
1776                     pointerCoords = pointerCoords!!,
1777                     buttonState = buttonState,
1778                     classification = motionEventClassification
1779                 )
1780 
1781             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
1782             androidComposeView.dispatchTouchEvent(downEvent)
1783         }
1784 
1785         // --> Assert
1786         rule.runOnUiThread {
1787             assertThat(pointerEvent).isNotNull()
1788             assertThat(pointerEvent!!.classification).isEqualTo(motionEventClassification)
1789         }
1790     }
1791 
1792     /*
1793      * Tests that [PointerEvent] without a [MotionEvent] will return a NONE classification.
1794      */
1795     @Test
1796     fun pointerInput_withoutMotionEvent_classificationShouldBeNone() {
1797         val pointerEventWithoutMotionEvent = PointerEvent(listOf(), internalPointerEvent = null)
1798 
1799         rule.runOnUiThread {
1800             assertThat(pointerEventWithoutMotionEvent.classification)
1801                 .isEqualTo(MotionEvent.CLASSIFICATION_NONE)
1802         }
1803     }
1804 
1805     /*
1806      * Tests alternating between hover TOUCH events and touch events across multiple UI elements.
1807      * Specifically, to recreate Talkback events.
1808      *
1809      * Important Note: Usually a hover MotionEvent sent from Android has the tool type set as
1810      * [MotionEvent.TOOL_TYPE_MOUSE]. However, Talkback sets the tool type to
1811      * [MotionEvent.TOOL_TYPE_FINGER]. We do that in this test by calling the
1812      * [dispatchTouchEvent()] instead of [dispatchMouseEvent()].
1813      *
1814      * Specific events:
1815      *  1. UI Element 1: ENTER (hover enter [touch])
1816      *  2. UI Element 1: EXIT (hover exit [touch])
1817      *  3. UI Element 1: PRESS (touch)
1818      *  4. UI Element 1: RELEASE (touch)
1819      *  5. UI Element 2: PRESS (touch)
1820      *  6. UI Element 2: RELEASE (touch)
1821      *
1822      * Should NOT trigger any additional events (like an extra press or exit)!
1823      */
1824     @Test
1825     fun alternatingHoverAndTouch_hoverUi1ToTouchUi1ToTouchUi2_shouldNotTriggerAdditionalEvents() {
1826         // --> Arrange
1827         var box1LayoutCoordinates: LayoutCoordinates? = null
1828         var box2LayoutCoordinates: LayoutCoordinates? = null
1829 
1830         val setUpFinishedLatch = CountDownLatch(4)
1831 
1832         var eventTime = 100
1833 
1834         // Events for Box 1
1835         var box1HoverEnter = 0
1836         var box1HoverExit = 0
1837         var box1Down = 0
1838         var box1Up = 0
1839 
1840         // Events for Box 2
1841         var box2Down = 0
1842         var box2Up = 0
1843 
1844         // All other events that should never be triggered in this test
1845         var eventsThatShouldNotTrigger = false
1846 
1847         var pointerEvent: PointerEvent? = null
1848 
1849         rule.runOnUiThread {
1850             container.setContent {
1851                 Column(
1852                     Modifier.fillMaxSize().onGloballyPositioned { setUpFinishedLatch.countDown() }
1853                 ) {
1854                     // Box 1
1855                     Box(
1856                         Modifier.size(50.dp)
1857                             .onGloballyPositioned {
1858                                 box1LayoutCoordinates = it
1859                                 setUpFinishedLatch.countDown()
1860                             }
1861                             .pointerInput(Unit) {
1862                                 awaitPointerEventScope {
1863                                     while (true) {
1864                                         pointerEvent = awaitPointerEvent()
1865 
1866                                         when (pointerEvent!!.type) {
1867                                             PointerEventType.Enter -> {
1868                                                 ++box1HoverEnter
1869                                             }
1870                                             PointerEventType.Press -> {
1871                                                 ++box1Down
1872                                             }
1873                                             PointerEventType.Release -> {
1874                                                 ++box1Up
1875                                             }
1876                                             PointerEventType.Exit -> {
1877                                                 ++box1HoverExit
1878                                             }
1879                                             else -> {
1880                                                 eventsThatShouldNotTrigger = true
1881                                             }
1882                                         }
1883                                     }
1884                                 }
1885                             }
1886                     ) {}
1887 
1888                     // Box 2
1889                     Box(
1890                         Modifier.size(50.dp)
1891                             .onGloballyPositioned {
1892                                 box2LayoutCoordinates = it
1893                                 setUpFinishedLatch.countDown()
1894                             }
1895                             .pointerInput(Unit) {
1896                                 awaitPointerEventScope {
1897                                     while (true) {
1898                                         pointerEvent = awaitPointerEvent()
1899 
1900                                         when (pointerEvent!!.type) {
1901                                             PointerEventType.Press -> {
1902                                                 ++box2Down
1903                                             }
1904                                             PointerEventType.Release -> {
1905                                                 ++box2Up
1906                                             }
1907                                             else -> {
1908                                                 eventsThatShouldNotTrigger = true
1909                                             }
1910                                         }
1911                                     }
1912                                 }
1913                             }
1914                     ) {}
1915 
1916                     // Box 3
1917                     Box(
1918                         Modifier.size(50.dp)
1919                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
1920                             .pointerInput(Unit) {
1921                                 awaitPointerEventScope {
1922                                     while (true) {
1923                                         pointerEvent = awaitPointerEvent()
1924                                         // Should never do anything with this UI element.
1925                                         eventsThatShouldNotTrigger = true
1926                                     }
1927                                 }
1928                             }
1929                     ) {}
1930                 }
1931             }
1932         }
1933         // Ensure Arrange (setup) step is finished
1934         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
1935 
1936         // --> Act + Assert (interwoven)
1937         // Hover Enter on Box 1
1938         dispatchTouchEvent(
1939             action = ACTION_HOVER_ENTER,
1940             layoutCoordinates = box1LayoutCoordinates!!,
1941             eventTime = eventTime
1942         )
1943         rule.runOnUiThread {
1944             // Verify Box 1 events
1945             assertThat(box1HoverEnter).isEqualTo(1)
1946             assertThat(box1HoverExit).isEqualTo(0)
1947             assertThat(box1Down).isEqualTo(0)
1948             assertThat(box1Up).isEqualTo(0)
1949 
1950             // Verify Box 2 events
1951             assertThat(box2Down).isEqualTo(0)
1952             assertThat(box2Up).isEqualTo(0)
1953 
1954             assertThat(pointerEvent).isNotNull()
1955             assertThat(eventsThatShouldNotTrigger).isFalse()
1956             assertHoverEvent(pointerEvent!!, isEnter = true)
1957         }
1958 
1959         // Hover Exit on Box 1
1960         pointerEvent = null // Reset before each event
1961         eventTime += 100
1962         dispatchTouchEvent(
1963             action = ACTION_HOVER_EXIT,
1964             layoutCoordinates = box1LayoutCoordinates!!,
1965             eventTime = eventTime
1966         )
1967 
1968         rule.waitForFutureFrame(2)
1969 
1970         rule.runOnUiThread {
1971             // Verify Box 1 events
1972             assertThat(box1HoverEnter).isEqualTo(1)
1973             assertThat(box1HoverExit).isEqualTo(1)
1974             assertThat(box1Down).isEqualTo(0)
1975             assertThat(box1Up).isEqualTo(0)
1976 
1977             // Verify Box 2 events
1978             assertThat(box2Down).isEqualTo(0)
1979             assertThat(box2Up).isEqualTo(0)
1980 
1981             assertThat(pointerEvent).isNotNull()
1982             assertThat(eventsThatShouldNotTrigger).isFalse()
1983         }
1984 
1985         // Press on Box 1
1986         pointerEvent = null // Reset before each event
1987         eventTime += 100
1988         dispatchTouchEvent(
1989             action = ACTION_DOWN,
1990             layoutCoordinates = box1LayoutCoordinates!!,
1991             eventTime = eventTime
1992         )
1993         rule.runOnUiThread {
1994             // Verify Box 1 events
1995             assertThat(box1HoverEnter).isEqualTo(1)
1996             assertThat(box1HoverExit).isEqualTo(1)
1997             assertThat(box1Down).isEqualTo(1)
1998             assertThat(box1Up).isEqualTo(0)
1999 
2000             // Verify Box 2 events
2001             assertThat(box2Down).isEqualTo(0)
2002             assertThat(box2Up).isEqualTo(0)
2003 
2004             assertThat(pointerEvent).isNotNull()
2005             assertThat(eventsThatShouldNotTrigger).isFalse()
2006         }
2007 
2008         // Release on Box 1
2009         pointerEvent = null // Reset before each event
2010         eventTime += 100
2011         dispatchTouchEvent(
2012             action = ACTION_UP,
2013             layoutCoordinates = box1LayoutCoordinates!!,
2014             eventTime = eventTime
2015         )
2016         rule.runOnUiThread {
2017             // Verify Box 1 events
2018             assertThat(box1HoverEnter).isEqualTo(1)
2019             assertThat(box1HoverExit).isEqualTo(1)
2020             assertThat(box1Down).isEqualTo(1)
2021             assertThat(box1Up).isEqualTo(1)
2022 
2023             // Verify Box 2 events
2024             assertThat(box2Down).isEqualTo(0)
2025             assertThat(box2Up).isEqualTo(0)
2026 
2027             assertThat(pointerEvent).isNotNull()
2028             assertThat(eventsThatShouldNotTrigger).isFalse()
2029         }
2030 
2031         // Press on Box 2
2032         pointerEvent = null // Reset before each event
2033         eventTime += 100
2034         dispatchTouchEvent(
2035             action = ACTION_DOWN,
2036             layoutCoordinates = box2LayoutCoordinates!!,
2037             eventTime = eventTime
2038         )
2039         rule.runOnUiThread {
2040             // Verify Box 1 events
2041             assertThat(box1HoverEnter).isEqualTo(1)
2042             assertThat(box1HoverExit).isEqualTo(1)
2043             assertThat(box1Down).isEqualTo(1)
2044             assertThat(box1Up).isEqualTo(1)
2045 
2046             // Verify Box 2 events
2047             assertThat(box2Down).isEqualTo(1)
2048             assertThat(box2Up).isEqualTo(0)
2049 
2050             assertThat(pointerEvent).isNotNull()
2051             assertThat(eventsThatShouldNotTrigger).isFalse()
2052         }
2053 
2054         // Press on Box 2
2055         pointerEvent = null // Reset before each event
2056         eventTime += 100
2057         dispatchTouchEvent(
2058             action = ACTION_UP,
2059             layoutCoordinates = box2LayoutCoordinates!!,
2060             eventTime = eventTime
2061         )
2062         rule.runOnUiThread {
2063             // Verify Box 1 events
2064             assertThat(box1HoverEnter).isEqualTo(1)
2065             assertThat(box1HoverExit).isEqualTo(1)
2066             assertThat(box1Down).isEqualTo(1)
2067             assertThat(box1Up).isEqualTo(1)
2068 
2069             // Verify Box 2 events
2070             assertThat(box2Down).isEqualTo(1)
2071             assertThat(box2Up).isEqualTo(1)
2072 
2073             assertThat(pointerEvent).isNotNull()
2074             assertThat(eventsThatShouldNotTrigger).isFalse()
2075         }
2076     }
2077 
2078     /*
2079      * Tests alternating between hover TOUCH events and regular touch events across multiple
2080      * UI elements. Specifically, to recreate Talkback events.
2081      *
2082      * Important Note: Usually a hover MotionEvent sent from Android has the tool type set as
2083      * [MotionEvent.TOOL_TYPE_MOUSE]. However, Talkback sets the tool type to
2084      * [MotionEvent.TOOL_TYPE_FINGER]. We do that in this test by calling the
2085      * [dispatchTouchEvent()] instead of [dispatchMouseEvent()].
2086      *
2087      * Specific events:
2088      *  1. UI Element 1: ENTER (hover enter [touch])
2089      *  2. UI Element 1: EXIT (hover exit [touch])
2090      *  5. UI Element 2: PRESS (touch)
2091      *  6. UI Element 2: RELEASE (touch)
2092      *
2093      * Should NOT trigger any additional events (like an extra exit)!
2094      */
2095     @Test
2096     fun alternatingHoverAndTouch_hoverUi1ToTouchUi2_shouldNotTriggerAdditionalEvents() {
2097         // --> Arrange
2098         var box1LayoutCoordinates: LayoutCoordinates? = null
2099         var box2LayoutCoordinates: LayoutCoordinates? = null
2100 
2101         val setUpFinishedLatch = CountDownLatch(4)
2102 
2103         var eventTime = 100
2104 
2105         // Events for Box 1
2106         var box1HoverEnter = 0
2107         var box1HoverExit = 0
2108 
2109         // Events for Box 2
2110         var box2Down = 0
2111         var box2Up = 0
2112 
2113         // All other events that should never be triggered in this test
2114         var eventsThatShouldNotTrigger = false
2115 
2116         var pointerEvent: PointerEvent? = null
2117 
2118         rule.runOnUiThread {
2119             container.setContent {
2120                 Column(
2121                     Modifier.fillMaxSize().onGloballyPositioned { setUpFinishedLatch.countDown() }
2122                 ) {
2123                     // Box 1
2124                     Box(
2125                         Modifier.size(50.dp)
2126                             .onGloballyPositioned {
2127                                 box1LayoutCoordinates = it
2128                                 setUpFinishedLatch.countDown()
2129                             }
2130                             .pointerInput(Unit) {
2131                                 awaitPointerEventScope {
2132                                     while (true) {
2133                                         pointerEvent = awaitPointerEvent()
2134 
2135                                         when (pointerEvent!!.type) {
2136                                             PointerEventType.Enter -> {
2137                                                 ++box1HoverEnter
2138                                             }
2139                                             PointerEventType.Exit -> {
2140                                                 ++box1HoverExit
2141                                             }
2142                                             else -> {
2143                                                 eventsThatShouldNotTrigger = true
2144                                             }
2145                                         }
2146                                     }
2147                                 }
2148                             }
2149                     ) {}
2150 
2151                     // Box 2
2152                     Box(
2153                         Modifier.size(50.dp)
2154                             .onGloballyPositioned {
2155                                 box2LayoutCoordinates = it
2156                                 setUpFinishedLatch.countDown()
2157                             }
2158                             .pointerInput(Unit) {
2159                                 awaitPointerEventScope {
2160                                     while (true) {
2161                                         pointerEvent = awaitPointerEvent()
2162 
2163                                         when (pointerEvent!!.type) {
2164                                             PointerEventType.Press -> {
2165                                                 ++box2Down
2166                                             }
2167                                             PointerEventType.Release -> {
2168                                                 ++box2Up
2169                                             }
2170                                             else -> {
2171                                                 eventsThatShouldNotTrigger = true
2172                                             }
2173                                         }
2174                                     }
2175                                 }
2176                             }
2177                     ) {}
2178 
2179                     // Box 3
2180                     Box(
2181                         Modifier.size(50.dp)
2182                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
2183                             .pointerInput(Unit) {
2184                                 awaitPointerEventScope {
2185                                     while (true) {
2186                                         pointerEvent = awaitPointerEvent()
2187                                         // Should never do anything with this UI element.
2188                                         eventsThatShouldNotTrigger = true
2189                                     }
2190                                 }
2191                             }
2192                     ) {}
2193                 }
2194             }
2195         }
2196         // Ensure Arrange (setup) step is finished
2197         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
2198 
2199         // --> Act + Assert (interwoven)
2200         // Hover Enter on Box 1
2201         dispatchTouchEvent(
2202             action = ACTION_HOVER_ENTER,
2203             layoutCoordinates = box1LayoutCoordinates!!,
2204             eventTime = eventTime
2205         )
2206         rule.runOnUiThread {
2207             // Verify Box 1 events
2208             assertThat(box1HoverEnter).isEqualTo(1)
2209             assertThat(box1HoverExit).isEqualTo(0)
2210 
2211             // Verify Box 2 events
2212             assertThat(box2Down).isEqualTo(0)
2213             assertThat(box2Up).isEqualTo(0)
2214 
2215             assertThat(pointerEvent).isNotNull()
2216             assertThat(eventsThatShouldNotTrigger).isFalse()
2217             assertHoverEvent(pointerEvent!!, isEnter = true)
2218         }
2219 
2220         // Hover Exit on Box 1
2221         pointerEvent = null // Reset before each event
2222         eventTime += 100
2223         dispatchTouchEvent(
2224             action = ACTION_HOVER_EXIT,
2225             layoutCoordinates = box1LayoutCoordinates!!,
2226             eventTime = eventTime
2227         )
2228 
2229         rule.waitForFutureFrame(2)
2230 
2231         rule.runOnUiThread {
2232             // Verify Box 1 events
2233             assertThat(box1HoverEnter).isEqualTo(1)
2234             assertThat(box1HoverExit).isEqualTo(1)
2235             // Verify Box 2 events
2236             assertThat(box2Down).isEqualTo(0)
2237             assertThat(box2Up).isEqualTo(0)
2238 
2239             assertThat(pointerEvent).isNotNull()
2240             assertThat(eventsThatShouldNotTrigger).isFalse()
2241         }
2242 
2243         // Press on Box 2
2244         pointerEvent = null // Reset before each event
2245         eventTime += 100
2246         dispatchTouchEvent(
2247             action = ACTION_DOWN,
2248             layoutCoordinates = box2LayoutCoordinates!!,
2249             eventTime = eventTime
2250         )
2251         rule.runOnUiThread {
2252             // Verify Box 1 events
2253             assertThat(box1HoverEnter).isEqualTo(1)
2254             assertThat(box1HoverExit).isEqualTo(1)
2255 
2256             // Verify Box 2 events
2257             assertThat(box2Down).isEqualTo(1)
2258             assertThat(box2Up).isEqualTo(0)
2259 
2260             assertThat(pointerEvent).isNotNull()
2261             assertThat(eventsThatShouldNotTrigger).isFalse()
2262         }
2263 
2264         // Press on Box 2
2265         pointerEvent = null // Reset before each event
2266         eventTime += 100
2267         dispatchTouchEvent(
2268             action = ACTION_UP,
2269             layoutCoordinates = box2LayoutCoordinates!!,
2270             eventTime = eventTime
2271         )
2272         rule.runOnUiThread {
2273             // Verify Box 1 events
2274             assertThat(box1HoverEnter).isEqualTo(1)
2275             assertThat(box1HoverExit).isEqualTo(1)
2276 
2277             // Verify Box 2 events
2278             assertThat(box2Down).isEqualTo(1)
2279             assertThat(box2Up).isEqualTo(1)
2280 
2281             assertThat(pointerEvent).isNotNull()
2282             assertThat(eventsThatShouldNotTrigger).isFalse()
2283         }
2284     }
2285 
2286     /*
2287      * Tests alternating hover TOUCH events across multiple UI elements. Specifically, to recreate
2288      * Talkback events.
2289      *
2290      * Important Note: Usually a hover MotionEvent sent from Android has the tool type set as
2291      * [MotionEvent.TOOL_TYPE_MOUSE]. However, Talkback sets the tool type to
2292      * [MotionEvent.TOOL_TYPE_FINGER]. We do that in this test by calling the
2293      * [dispatchTouchEvent()] instead of [dispatchMouseEvent()].
2294      *
2295      * Specific events:
2296      *  1. UI Element 1: ENTER (hover enter [touch])
2297      *  2. UI Element 1: EXIT (hover exit [touch])
2298      *  5. UI Element 2: ENTER (hover enter [touch])
2299      *  6. UI Element 2: EXIT (hover exit [touch])
2300      *
2301      * Should NOT trigger any additional events (like an extra exit)!
2302      */
2303     @Test
2304     fun hoverEventsBetweenUIElements_hoverUi1ToHoverUi2_shouldNotTriggerAdditionalEvents() {
2305         // --> Arrange
2306         var box1LayoutCoordinates: LayoutCoordinates? = null
2307         var box2LayoutCoordinates: LayoutCoordinates? = null
2308 
2309         val setUpFinishedLatch = CountDownLatch(4)
2310 
2311         // Events for Box 1
2312         var box1HoverEnter = 0
2313         var box1HoverExit = 0
2314 
2315         // Events for Box 2
2316         var box2HoverEnter = 0
2317         var box2HoverExit = 0
2318 
2319         // All other events that should never be triggered in this test
2320         var eventsThatShouldNotTrigger = false
2321 
2322         var pointerEvent: PointerEvent? = null
2323 
2324         rule.runOnUiThread {
2325             container.setContent {
2326                 Column(
2327                     Modifier.fillMaxSize().onGloballyPositioned { setUpFinishedLatch.countDown() }
2328                 ) {
2329                     // Box 1
2330                     Box(
2331                         Modifier.size(50.dp)
2332                             .onGloballyPositioned {
2333                                 box1LayoutCoordinates = it
2334                                 setUpFinishedLatch.countDown()
2335                             }
2336                             .pointerInput(Unit) {
2337                                 awaitPointerEventScope {
2338                                     while (true) {
2339                                         pointerEvent = awaitPointerEvent()
2340 
2341                                         when (pointerEvent!!.type) {
2342                                             PointerEventType.Enter -> {
2343                                                 ++box1HoverEnter
2344                                             }
2345                                             PointerEventType.Exit -> {
2346                                                 ++box1HoverExit
2347                                             }
2348                                             else -> {
2349                                                 eventsThatShouldNotTrigger = true
2350                                             }
2351                                         }
2352                                     }
2353                                 }
2354                             }
2355                     ) {}
2356 
2357                     // Box 2
2358                     Box(
2359                         Modifier.size(50.dp)
2360                             .onGloballyPositioned {
2361                                 box2LayoutCoordinates = it
2362                                 setUpFinishedLatch.countDown()
2363                             }
2364                             .pointerInput(Unit) {
2365                                 awaitPointerEventScope {
2366                                     while (true) {
2367                                         pointerEvent = awaitPointerEvent()
2368 
2369                                         when (pointerEvent!!.type) {
2370                                             PointerEventType.Enter -> {
2371                                                 ++box2HoverEnter
2372                                             }
2373                                             PointerEventType.Exit -> {
2374                                                 ++box2HoverExit
2375                                             }
2376                                             else -> {
2377                                                 eventsThatShouldNotTrigger = true
2378                                             }
2379                                         }
2380                                     }
2381                                 }
2382                             }
2383                     ) {}
2384 
2385                     // Box 3
2386                     Box(
2387                         Modifier.size(50.dp)
2388                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
2389                             .pointerInput(Unit) {
2390                                 awaitPointerEventScope {
2391                                     while (true) {
2392                                         pointerEvent = awaitPointerEvent()
2393                                         // Should never do anything with this UI element.
2394                                         eventsThatShouldNotTrigger = true
2395                                     }
2396                                 }
2397                             }
2398                     ) {}
2399                 }
2400             }
2401         }
2402         // Ensure Arrange (setup) step is finished
2403         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
2404 
2405         // --> Act + Assert (interwoven)
2406         // Hover Enter on Box 1
2407         dispatchTouchEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
2408         rule.runOnUiThread {
2409             // Verify Box 1 events
2410             assertThat(box1HoverEnter).isEqualTo(1)
2411             assertThat(box1HoverExit).isEqualTo(0)
2412 
2413             // Verify Box 2 events
2414             assertThat(box2HoverEnter).isEqualTo(0)
2415             assertThat(box2HoverExit).isEqualTo(0)
2416 
2417             assertThat(pointerEvent).isNotNull()
2418             assertThat(eventsThatShouldNotTrigger).isFalse()
2419             assertHoverEvent(pointerEvent!!, isEnter = true)
2420         }
2421 
2422         // Hover Exit on Box 1
2423         pointerEvent = null // Reset before each event
2424         dispatchTouchEvent(ACTION_HOVER_EXIT, box1LayoutCoordinates!!)
2425 
2426         rule.waitForFutureFrame(2)
2427 
2428         rule.runOnUiThread {
2429             // Verify Box 1 events
2430             assertThat(box1HoverEnter).isEqualTo(1)
2431             assertThat(box1HoverExit).isEqualTo(1)
2432             // Verify Box 2 events
2433             assertThat(box2HoverEnter).isEqualTo(0)
2434             assertThat(box2HoverExit).isEqualTo(0)
2435 
2436             assertThat(pointerEvent).isNotNull()
2437             assertThat(eventsThatShouldNotTrigger).isFalse()
2438         }
2439 
2440         // Press on Box 2
2441         pointerEvent = null // Reset before each event
2442         dispatchTouchEvent(ACTION_HOVER_ENTER, box2LayoutCoordinates!!)
2443         rule.runOnUiThread {
2444             // Verify Box 1 events
2445             assertThat(box1HoverEnter).isEqualTo(1)
2446             assertThat(box1HoverExit).isEqualTo(1)
2447 
2448             // Verify Box 2 events
2449             assertThat(box2HoverEnter).isEqualTo(1)
2450             assertThat(box2HoverExit).isEqualTo(0)
2451 
2452             assertThat(pointerEvent).isNotNull()
2453             assertThat(eventsThatShouldNotTrigger).isFalse()
2454         }
2455 
2456         // Press on Box 2
2457         pointerEvent = null // Reset before each event
2458         dispatchTouchEvent(ACTION_HOVER_EXIT, box2LayoutCoordinates!!)
2459 
2460         rule.waitForFutureFrame(2)
2461 
2462         rule.runOnUiThread {
2463             // Verify Box 1 events
2464             assertThat(box1HoverEnter).isEqualTo(1)
2465             assertThat(box1HoverExit).isEqualTo(1)
2466 
2467             // Verify Box 2 events
2468             assertThat(box2HoverEnter).isEqualTo(1)
2469             assertThat(box2HoverExit).isEqualTo(1)
2470 
2471             assertThat(pointerEvent).isNotNull()
2472             assertThat(eventsThatShouldNotTrigger).isFalse()
2473         }
2474     }
2475 
2476     /*
2477      * Tests TOUCH events are triggered correctly when dynamically adding a NON-pointer input
2478      * modifier above an existing pointer input modifier.
2479      *
2480      * Note: The lambda for the existing pointer input modifier is not re-executed after the
2481      * dynamic one is added.
2482      *
2483      * Specific events:
2484      *  1. UI Element (modifier 1 only): PRESS (touch)
2485      *  2. UI Element (modifier 1 only): MOVE (touch)
2486      *  3. UI Element (modifier 1 only): RELEASE (touch)
2487      *  4. Dynamically adds NON-pointer input modifier (between input event streams)
2488      *  5. UI Element (modifier 1 and 2): PRESS (touch)
2489      *  6. UI Element (modifier 1 and 2): MOVE (touch)
2490      *  7. UI Element (modifier 1 and 2): RELEASE (touch)
2491      */
2492     @Test
2493     fun dynamicNonInputModifier_addsAboveExistingModifier_shouldTriggerInNewModifier() {
2494         // --> Arrange
2495         val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
2496         var box1LayoutCoordinates: LayoutCoordinates? = null
2497 
2498         val setUpFinishedLatch = CountDownLatch(1)
2499 
2500         var enableDynamicPointerInput by mutableStateOf(false)
2501 
2502         // Events for the lower modifier Box 1
2503         var originalPointerInputScopeExecutionCount by mutableStateOf(0)
2504         var preexistingModifierPress by mutableStateOf(0)
2505         var preexistingModifierMove by mutableStateOf(0)
2506         var preexistingModifierRelease by mutableStateOf(0)
2507 
2508         // All other events that should never be triggered in this test
2509         var eventsThatShouldNotTrigger by mutableStateOf(false)
2510 
2511         var pointerEvent: PointerEvent? by mutableStateOf(null)
2512 
2513         // Events for the dynamic upper modifier Box 1
2514         var dynamicModifierExecuted by mutableStateOf(false)
2515 
2516         // Non-Pointer Input Modifier that is toggled on/off based on passed value.
2517         fun Modifier.dynamicallyToggledModifier(enable: Boolean) =
2518             if (enable) {
2519                 dynamicModifierExecuted = true
2520                 background(Color.Green)
2521             } else this
2522 
2523         // Setup UI
2524         rule.runOnUiThread {
2525             container.setContent {
2526                 Box(
2527                     Modifier.size(200.dp)
2528                         .onGloballyPositioned {
2529                             box1LayoutCoordinates = it
2530                             setUpFinishedLatch.countDown()
2531                         }
2532                         .dynamicallyToggledModifier(enableDynamicPointerInput)
2533                         .pointerInput(originalPointerInputModifierKey) {
2534                             ++originalPointerInputScopeExecutionCount
2535                             // Reset pointer events when lambda is ran the first time
2536                             preexistingModifierPress = 0
2537                             preexistingModifierMove = 0
2538                             preexistingModifierRelease = 0
2539 
2540                             awaitPointerEventScope {
2541                                 while (true) {
2542                                     pointerEvent = awaitPointerEvent()
2543                                     when (pointerEvent!!.type) {
2544                                         PointerEventType.Press -> {
2545                                             ++preexistingModifierPress
2546                                         }
2547                                         PointerEventType.Move -> {
2548                                             ++preexistingModifierMove
2549                                         }
2550                                         PointerEventType.Release -> {
2551                                             ++preexistingModifierRelease
2552                                         }
2553                                         else -> {
2554                                             eventsThatShouldNotTrigger = true
2555                                         }
2556                                     }
2557                                 }
2558                             }
2559                         }
2560                 ) {}
2561             }
2562         }
2563         // Ensure Arrange (setup) step is finished
2564         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
2565 
2566         // --> Act + Assert (interwoven)
2567         // DOWN (original modifier only)
2568         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
2569         rule.runOnUiThread {
2570             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2571             assertThat(dynamicModifierExecuted).isEqualTo(false)
2572 
2573             // Verify Box 1 existing modifier events
2574             assertThat(preexistingModifierPress).isEqualTo(1)
2575             assertThat(preexistingModifierMove).isEqualTo(0)
2576             assertThat(preexistingModifierRelease).isEqualTo(0)
2577 
2578             assertThat(pointerEvent).isNotNull()
2579             assertThat(eventsThatShouldNotTrigger).isFalse()
2580         }
2581 
2582         // MOVE (original modifier only)
2583         dispatchTouchEvent(
2584             ACTION_MOVE,
2585             box1LayoutCoordinates!!,
2586             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2587         )
2588         rule.runOnUiThread {
2589             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2590             assertThat(dynamicModifierExecuted).isEqualTo(false)
2591 
2592             // Verify Box 1 existing modifier events
2593             assertThat(preexistingModifierPress).isEqualTo(1)
2594             assertThat(preexistingModifierMove).isEqualTo(1)
2595             assertThat(preexistingModifierRelease).isEqualTo(0)
2596 
2597             assertThat(pointerEvent).isNotNull()
2598             assertThat(eventsThatShouldNotTrigger).isFalse()
2599         }
2600 
2601         // UP (original modifier only)
2602         dispatchTouchEvent(
2603             ACTION_UP,
2604             box1LayoutCoordinates!!,
2605             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2606         )
2607         rule.runOnUiThread {
2608             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2609             assertThat(dynamicModifierExecuted).isEqualTo(false)
2610 
2611             // Verify Box 1 existing modifier events
2612             assertThat(preexistingModifierPress).isEqualTo(1)
2613             assertThat(preexistingModifierMove).isEqualTo(1)
2614             assertThat(preexistingModifierRelease).isEqualTo(1)
2615 
2616             assertThat(pointerEvent).isNotNull()
2617             assertThat(eventsThatShouldNotTrigger).isFalse()
2618         }
2619         enableDynamicPointerInput = true
2620         rule.waitForFutureFrame(2)
2621 
2622         // DOWN (original + dynamically added modifiers)
2623         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
2624 
2625         rule.runOnUiThread {
2626             // There are no pointer input modifiers added above this pointer modifier, so the
2627             // same one is used.
2628             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2629             // The dynamic one has been added, so we execute its thing as well.
2630             assertThat(dynamicModifierExecuted).isEqualTo(true)
2631 
2632             // Verify Box 1 existing modifier events
2633             assertThat(preexistingModifierPress).isEqualTo(2)
2634             assertThat(preexistingModifierMove).isEqualTo(1)
2635             assertThat(preexistingModifierRelease).isEqualTo(1)
2636 
2637             assertThat(pointerEvent).isNotNull()
2638             assertThat(eventsThatShouldNotTrigger).isFalse()
2639         }
2640 
2641         // MOVE (original + dynamically added modifiers)
2642         dispatchTouchEvent(
2643             ACTION_MOVE,
2644             box1LayoutCoordinates!!,
2645             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2646         )
2647         rule.runOnUiThread {
2648             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2649             assertThat(dynamicModifierExecuted).isEqualTo(true)
2650 
2651             // Verify Box 1 existing modifier events
2652             assertThat(preexistingModifierPress).isEqualTo(2)
2653             assertThat(preexistingModifierMove).isEqualTo(2)
2654             assertThat(preexistingModifierRelease).isEqualTo(1)
2655 
2656             assertThat(pointerEvent).isNotNull()
2657             assertThat(eventsThatShouldNotTrigger).isFalse()
2658         }
2659 
2660         // UP (original + dynamically added modifiers)
2661         dispatchTouchEvent(
2662             ACTION_UP,
2663             box1LayoutCoordinates!!,
2664             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2665         )
2666         rule.runOnUiThread {
2667             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2668             assertThat(dynamicModifierExecuted).isEqualTo(true)
2669 
2670             // Verify Box 1 existing modifier events
2671             assertThat(preexistingModifierPress).isEqualTo(2)
2672             assertThat(preexistingModifierMove).isEqualTo(2)
2673             assertThat(preexistingModifierRelease).isEqualTo(2)
2674 
2675             assertThat(pointerEvent).isNotNull()
2676             assertThat(eventsThatShouldNotTrigger).isFalse()
2677         }
2678     }
2679 
2680     /*
2681      * Tests TOUCH events are triggered correctly when dynamically adding a pointer input modifier
2682      * ABOVE an existing pointer input modifier.
2683      *
2684      * Note: The lambda for the existing pointer input modifier **IS** re-executed after the
2685      * dynamic pointer input modifier is added above it.
2686      *
2687      * Specific events:
2688      *  1. UI Element (modifier 1 only): PRESS (touch)
2689      *  2. UI Element (modifier 1 only): MOVE (touch)
2690      *  3. UI Element (modifier 1 only): RELEASE (touch)
2691      *  4. Dynamically add pointer input modifier above existing one (between input event streams)
2692      *  5. UI Element (modifier 1 and 2): PRESS (touch)
2693      *  6. UI Element (modifier 1 and 2): MOVE (touch)
2694      *  7. UI Element (modifier 1 and 2): RELEASE (touch)
2695      */
2696     @Test
2697     fun dynamicInputModifierWithKey_addsAboveExistingModifier_shouldTriggerInNewModifier() {
2698         // --> Arrange
2699         val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
2700         var box1LayoutCoordinates: LayoutCoordinates? = null
2701 
2702         val setUpFinishedLatch = CountDownLatch(1)
2703 
2704         var enableDynamicPointerInput by mutableStateOf(false)
2705 
2706         // Events for the lower modifier Box 1
2707         var originalPointerInputScopeExecutionCount by mutableStateOf(0)
2708         var preexistingModifierPress by mutableStateOf(0)
2709         var preexistingModifierMove by mutableStateOf(0)
2710         var preexistingModifierRelease by mutableStateOf(0)
2711 
2712         // Events for the dynamic upper modifier Box 1
2713         var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
2714         var dynamicModifierPress by mutableStateOf(0)
2715         var dynamicModifierMove by mutableStateOf(0)
2716         var dynamicModifierRelease by mutableStateOf(0)
2717 
2718         // All other events that should never be triggered in this test
2719         var eventsThatShouldNotTrigger by mutableStateOf(false)
2720 
2721         var pointerEvent: PointerEvent? by mutableStateOf(null)
2722 
2723         // Pointer Input Modifier that is toggled on/off based on passed value.
2724         fun Modifier.dynamicallyToggledPointerInput(
2725             enable: Boolean,
2726             pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
2727         ) =
2728             if (enable) {
2729                 pointerInput(pointerEventLambda) {
2730                     ++dynamicPointerInputScopeExecutionCount
2731 
2732                     // Reset pointer events when lambda is ran the first time
2733                     dynamicModifierPress = 0
2734                     dynamicModifierMove = 0
2735                     dynamicModifierRelease = 0
2736 
2737                     awaitPointerEventScope {
2738                         while (true) {
2739                             pointerEventLambda(awaitPointerEvent())
2740                         }
2741                     }
2742                 }
2743             } else this
2744 
2745         // Setup UI
2746         rule.runOnUiThread {
2747             container.setContent {
2748                 Box(
2749                     Modifier.size(200.dp)
2750                         .onGloballyPositioned {
2751                             box1LayoutCoordinates = it
2752                             setUpFinishedLatch.countDown()
2753                         }
2754                         .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
2755                             when (it.type) {
2756                                 PointerEventType.Press -> {
2757                                     ++dynamicModifierPress
2758                                 }
2759                                 PointerEventType.Move -> {
2760                                     ++dynamicModifierMove
2761                                 }
2762                                 PointerEventType.Release -> {
2763                                     ++dynamicModifierRelease
2764                                 }
2765                                 else -> {
2766                                     eventsThatShouldNotTrigger = true
2767                                 }
2768                             }
2769                         }
2770                         .pointerInput(originalPointerInputModifierKey) {
2771                             ++originalPointerInputScopeExecutionCount
2772                             // Reset pointer events when lambda is ran the first time
2773                             preexistingModifierPress = 0
2774                             preexistingModifierMove = 0
2775                             preexistingModifierRelease = 0
2776 
2777                             awaitPointerEventScope {
2778                                 while (true) {
2779                                     pointerEvent = awaitPointerEvent()
2780                                     when (pointerEvent!!.type) {
2781                                         PointerEventType.Press -> {
2782                                             ++preexistingModifierPress
2783                                         }
2784                                         PointerEventType.Move -> {
2785                                             ++preexistingModifierMove
2786                                         }
2787                                         PointerEventType.Release -> {
2788                                             ++preexistingModifierRelease
2789                                         }
2790                                         else -> {
2791                                             eventsThatShouldNotTrigger = true
2792                                         }
2793                                     }
2794                                 }
2795                             }
2796                         }
2797                 ) {}
2798             }
2799         }
2800         // Ensure Arrange (setup) step is finished
2801         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
2802 
2803         // --> Act + Assert (interwoven)
2804         // DOWN (original modifier only)
2805         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
2806         rule.runOnUiThread {
2807             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2808             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
2809 
2810             // Verify Box 1 existing modifier events
2811             assertThat(preexistingModifierPress).isEqualTo(1)
2812             assertThat(preexistingModifierMove).isEqualTo(0)
2813             assertThat(preexistingModifierRelease).isEqualTo(0)
2814 
2815             // Verify Box 1 dynamically added modifier events
2816             assertThat(dynamicModifierPress).isEqualTo(0)
2817             assertThat(dynamicModifierMove).isEqualTo(0)
2818             assertThat(dynamicModifierRelease).isEqualTo(0)
2819 
2820             assertThat(pointerEvent).isNotNull()
2821             assertThat(eventsThatShouldNotTrigger).isFalse()
2822         }
2823 
2824         // MOVE (original modifier only)
2825         dispatchTouchEvent(
2826             ACTION_MOVE,
2827             box1LayoutCoordinates!!,
2828             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2829         )
2830         rule.runOnUiThread {
2831             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2832             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
2833 
2834             // Verify Box 1 existing modifier events
2835             assertThat(preexistingModifierPress).isEqualTo(1)
2836             assertThat(preexistingModifierMove).isEqualTo(1)
2837             assertThat(preexistingModifierRelease).isEqualTo(0)
2838 
2839             // Verify Box 1 dynamically added modifier events
2840             assertThat(dynamicModifierPress).isEqualTo(0)
2841             assertThat(dynamicModifierMove).isEqualTo(0)
2842             assertThat(dynamicModifierRelease).isEqualTo(0)
2843 
2844             assertThat(pointerEvent).isNotNull()
2845             assertThat(eventsThatShouldNotTrigger).isFalse()
2846         }
2847 
2848         // UP (original modifier only)
2849         dispatchTouchEvent(
2850             ACTION_UP,
2851             box1LayoutCoordinates!!,
2852             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2853         )
2854         rule.runOnUiThread {
2855             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2856             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
2857 
2858             // Verify Box 1 existing modifier events
2859             assertThat(preexistingModifierPress).isEqualTo(1)
2860             assertThat(preexistingModifierMove).isEqualTo(1)
2861             assertThat(preexistingModifierRelease).isEqualTo(1)
2862 
2863             // Verify Box 1 dynamically added modifier events
2864             assertThat(dynamicModifierPress).isEqualTo(0)
2865             assertThat(dynamicModifierMove).isEqualTo(0)
2866             assertThat(dynamicModifierRelease).isEqualTo(0)
2867 
2868             assertThat(pointerEvent).isNotNull()
2869             assertThat(eventsThatShouldNotTrigger).isFalse()
2870         }
2871 
2872         enableDynamicPointerInput = true
2873         rule.waitForFutureFrame(2)
2874 
2875         // Important Note: Even though we reset all the pointer input blocks, the initial lambda is
2876         // lazily executed, meaning it won't reset the values until the first event comes in, so
2877         // the previously set values are still the same until an event comes in.
2878         rule.runOnUiThread {
2879             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
2880             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
2881 
2882             assertThat(preexistingModifierPress).isEqualTo(1)
2883             assertThat(preexistingModifierMove).isEqualTo(1)
2884             assertThat(preexistingModifierRelease).isEqualTo(1)
2885 
2886             assertThat(dynamicModifierPress).isEqualTo(0)
2887             assertThat(dynamicModifierMove).isEqualTo(0)
2888             assertThat(dynamicModifierRelease).isEqualTo(0)
2889         }
2890 
2891         // DOWN (original + dynamically added modifiers)
2892         // Now an event comes in, so the lambdas are both executed completely (dynamic one for the
2893         // first time and the existing one for a second time [since it was moved]).
2894         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
2895 
2896         rule.runOnUiThread {
2897             // While the original pointer input block is being reused after a new one is added, it
2898             // is reset (since things have changed with the Modifiers), so the entire block is
2899             // executed again to allow devs to reset their gesture detectors for the new Modifier
2900             // chain changes.
2901             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
2902             // The dynamic one has been added, so we execute its thing as well.
2903             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
2904 
2905             // Verify Box 1 existing modifier events
2906             assertThat(preexistingModifierPress).isEqualTo(1)
2907             assertThat(preexistingModifierMove).isEqualTo(0)
2908             assertThat(preexistingModifierRelease).isEqualTo(0)
2909 
2910             // Verify Box 1 dynamically added modifier events
2911             assertThat(dynamicModifierPress).isEqualTo(1)
2912             assertThat(dynamicModifierMove).isEqualTo(0)
2913             assertThat(dynamicModifierRelease).isEqualTo(0)
2914 
2915             assertThat(pointerEvent).isNotNull()
2916             assertThat(eventsThatShouldNotTrigger).isFalse()
2917         }
2918 
2919         // MOVE (original + dynamically added modifiers)
2920         dispatchTouchEvent(
2921             ACTION_MOVE,
2922             box1LayoutCoordinates!!,
2923             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2924         )
2925         rule.runOnUiThread {
2926             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
2927             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
2928 
2929             // Verify Box 1 existing modifier events
2930             assertThat(preexistingModifierPress).isEqualTo(1)
2931             assertThat(preexistingModifierMove).isEqualTo(1)
2932             assertThat(preexistingModifierRelease).isEqualTo(0)
2933 
2934             // Verify Box 1 dynamically added modifier events
2935             assertThat(dynamicModifierPress).isEqualTo(1)
2936             assertThat(dynamicModifierMove).isEqualTo(1)
2937             assertThat(dynamicModifierRelease).isEqualTo(0)
2938 
2939             assertThat(pointerEvent).isNotNull()
2940             assertThat(eventsThatShouldNotTrigger).isFalse()
2941         }
2942 
2943         // UP (original + dynamically added modifiers)
2944         dispatchTouchEvent(
2945             ACTION_UP,
2946             box1LayoutCoordinates!!,
2947             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
2948         )
2949         rule.runOnUiThread {
2950             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
2951             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
2952 
2953             // Verify Box 1 existing modifier events
2954             assertThat(preexistingModifierPress).isEqualTo(1)
2955             assertThat(preexistingModifierMove).isEqualTo(1)
2956             assertThat(preexistingModifierRelease).isEqualTo(1)
2957 
2958             // Verify Box 1 dynamically added modifier events
2959             assertThat(dynamicModifierPress).isEqualTo(1)
2960             assertThat(dynamicModifierMove).isEqualTo(1)
2961             assertThat(dynamicModifierRelease).isEqualTo(1)
2962 
2963             assertThat(pointerEvent).isNotNull()
2964             assertThat(eventsThatShouldNotTrigger).isFalse()
2965         }
2966     }
2967 
2968     /*
2969      * Tests TOUCH events are triggered correctly when dynamically adding a pointer input modifier
2970      * (which uses Unit for its key) ABOVE an existing pointer input modifier.
2971      *
2972      * Specific events:
2973      *  1. UI Element (modifier 1 only): PRESS (touch)
2974      *  2. UI Element (modifier 1 only): MOVE (touch)
2975      *  3. UI Element (modifier 1 only): RELEASE (touch)
2976      *  4. Dynamically add pointer input modifier above existing one (between input event streams)
2977      *  5. UI Element (modifier 1 and 2): PRESS (touch)
2978      *  6. UI Element (modifier 1 and 2): MOVE (touch)
2979      *  7. UI Element (modifier 1 and 2): RELEASE (touch)
2980      */
2981     @Test
2982     fun dynamicInputModifierWithUnitKey_addsAboveExistingModifier_triggersBothModifiers() {
2983         // --> Arrange
2984         var box1LayoutCoordinates: LayoutCoordinates? = null
2985 
2986         val setUpFinishedLatch = CountDownLatch(1)
2987 
2988         var enableDynamicPointerInput by mutableStateOf(false)
2989 
2990         // Events for the lower modifier Box 1
2991         var originalPointerInputScopeExecutionCount by mutableStateOf(0)
2992         var preexistingModifierPress by mutableStateOf(0)
2993         var preexistingModifierMove by mutableStateOf(0)
2994         var preexistingModifierRelease by mutableStateOf(0)
2995 
2996         // Events for the dynamic upper modifier Box 1
2997         var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
2998         var dynamicModifierPress by mutableStateOf(0)
2999         var dynamicModifierMove by mutableStateOf(0)
3000         var dynamicModifierRelease by mutableStateOf(0)
3001 
3002         // All other events that should never be triggered in this test
3003         var eventsThatShouldNotTrigger by mutableStateOf(false)
3004 
3005         var pointerEvent: PointerEvent? by mutableStateOf(null)
3006 
3007         // Pointer Input Modifier that is toggled on/off based on passed value.
3008         fun Modifier.dynamicallyToggledPointerInput(
3009             enable: Boolean,
3010             pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
3011         ) =
3012             if (enable) {
3013                 pointerInput(Unit) {
3014                     ++dynamicPointerInputScopeExecutionCount
3015 
3016                     // Reset pointer events when lambda is ran the first time
3017                     dynamicModifierPress = 0
3018                     dynamicModifierMove = 0
3019                     dynamicModifierRelease = 0
3020 
3021                     awaitPointerEventScope {
3022                         while (true) {
3023                             pointerEventLambda(awaitPointerEvent())
3024                         }
3025                     }
3026                 }
3027             } else this
3028 
3029         // Setup UI
3030         rule.runOnUiThread {
3031             container.setContent {
3032                 Box(
3033                     Modifier.size(200.dp)
3034                         .onGloballyPositioned {
3035                             box1LayoutCoordinates = it
3036                             setUpFinishedLatch.countDown()
3037                         }
3038                         .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
3039                             when (it.type) {
3040                                 PointerEventType.Press -> {
3041                                     ++dynamicModifierPress
3042                                 }
3043                                 PointerEventType.Move -> {
3044                                     ++dynamicModifierMove
3045                                 }
3046                                 PointerEventType.Release -> {
3047                                     ++dynamicModifierRelease
3048                                 }
3049                                 else -> {
3050                                     eventsThatShouldNotTrigger = true
3051                                 }
3052                             }
3053                         }
3054                         .pointerInput(Unit) {
3055                             ++originalPointerInputScopeExecutionCount
3056                             awaitPointerEventScope {
3057                                 while (true) {
3058                                     pointerEvent = awaitPointerEvent()
3059                                     when (pointerEvent!!.type) {
3060                                         PointerEventType.Press -> {
3061                                             ++preexistingModifierPress
3062                                         }
3063                                         PointerEventType.Move -> {
3064                                             ++preexistingModifierMove
3065                                         }
3066                                         PointerEventType.Release -> {
3067                                             ++preexistingModifierRelease
3068                                         }
3069                                         else -> {
3070                                             eventsThatShouldNotTrigger = true
3071                                         }
3072                                     }
3073                                 }
3074                             }
3075                         }
3076                 ) {}
3077             }
3078         }
3079         // Ensure Arrange (setup) step is finished
3080         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
3081 
3082         // --> Act + Assert (interwoven)
3083         // DOWN (original modifier only)
3084         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
3085         rule.runOnUiThread {
3086             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3087             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
3088 
3089             // Verify Box 1 existing modifier events
3090             assertThat(preexistingModifierPress).isEqualTo(1)
3091             assertThat(preexistingModifierMove).isEqualTo(0)
3092             assertThat(preexistingModifierRelease).isEqualTo(0)
3093 
3094             // Verify Box 1 dynamically added modifier events
3095             assertThat(dynamicModifierPress).isEqualTo(0)
3096             assertThat(dynamicModifierMove).isEqualTo(0)
3097             assertThat(dynamicModifierRelease).isEqualTo(0)
3098 
3099             assertThat(pointerEvent).isNotNull()
3100             assertThat(eventsThatShouldNotTrigger).isFalse()
3101         }
3102 
3103         // MOVE (original modifier only)
3104         dispatchTouchEvent(
3105             ACTION_MOVE,
3106             box1LayoutCoordinates!!,
3107             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3108         )
3109         rule.runOnUiThread {
3110             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3111             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
3112 
3113             // Verify Box 1 existing modifier events
3114             assertThat(preexistingModifierPress).isEqualTo(1)
3115             assertThat(preexistingModifierMove).isEqualTo(1)
3116             assertThat(preexistingModifierRelease).isEqualTo(0)
3117 
3118             // Verify Box 1 dynamically added modifier events
3119             assertThat(dynamicModifierPress).isEqualTo(0)
3120             assertThat(dynamicModifierMove).isEqualTo(0)
3121             assertThat(dynamicModifierRelease).isEqualTo(0)
3122 
3123             assertThat(pointerEvent).isNotNull()
3124             assertThat(eventsThatShouldNotTrigger).isFalse()
3125         }
3126 
3127         // UP (original modifier only)
3128         dispatchTouchEvent(
3129             ACTION_UP,
3130             box1LayoutCoordinates!!,
3131             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3132         )
3133         rule.runOnUiThread {
3134             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3135             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
3136 
3137             // Verify Box 1 existing modifier events
3138             assertThat(preexistingModifierPress).isEqualTo(1)
3139             assertThat(preexistingModifierMove).isEqualTo(1)
3140             assertThat(preexistingModifierRelease).isEqualTo(1)
3141 
3142             // Verify Box 1 dynamically added modifier events
3143             assertThat(dynamicModifierPress).isEqualTo(0)
3144             assertThat(dynamicModifierMove).isEqualTo(0)
3145             assertThat(dynamicModifierRelease).isEqualTo(0)
3146 
3147             assertThat(pointerEvent).isNotNull()
3148             assertThat(eventsThatShouldNotTrigger).isFalse()
3149         }
3150 
3151         enableDynamicPointerInput = true
3152         rule.waitForFutureFrame(2)
3153 
3154         // Important Note: I'm not resetting the variable counters in this test.
3155 
3156         // DOWN (original + dynamically added modifiers)
3157         // Now an event comes in, so the lambdas are both executed completely for the first time.
3158         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
3159 
3160         rule.runOnUiThread {
3161             // While the original pointer input block is being reused after a new one is added, it
3162             // is reset (since things have changed with the Modifiers), so the entire block is
3163             // executed again to allow devs to reset their gesture detectors for the new Modifier
3164             // chain changes.
3165             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
3166             // The dynamic one has been added, so we execute its lambda as well.
3167             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
3168 
3169             // Verify Box 1 existing modifier events
3170             assertThat(preexistingModifierPress).isEqualTo(2)
3171             assertThat(preexistingModifierMove).isEqualTo(1)
3172             assertThat(preexistingModifierRelease).isEqualTo(1)
3173 
3174             // Verify Box 1 dynamically added modifier events
3175             assertThat(dynamicModifierPress).isEqualTo(1)
3176             assertThat(dynamicModifierMove).isEqualTo(0)
3177             assertThat(dynamicModifierRelease).isEqualTo(0)
3178 
3179             assertThat(pointerEvent).isNotNull()
3180             assertThat(eventsThatShouldNotTrigger).isFalse()
3181         }
3182 
3183         // MOVE (original + dynamically added modifiers)
3184         dispatchTouchEvent(
3185             ACTION_MOVE,
3186             box1LayoutCoordinates!!,
3187             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3188         )
3189         rule.runOnUiThread {
3190             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
3191             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
3192 
3193             // Verify Box 1 existing modifier events
3194             assertThat(preexistingModifierPress).isEqualTo(2)
3195             assertThat(preexistingModifierMove).isEqualTo(2)
3196             assertThat(preexistingModifierRelease).isEqualTo(1)
3197 
3198             // Verify Box 1 dynamically added modifier events
3199             assertThat(dynamicModifierPress).isEqualTo(1)
3200             assertThat(dynamicModifierMove).isEqualTo(1)
3201             assertThat(dynamicModifierRelease).isEqualTo(0)
3202 
3203             assertThat(pointerEvent).isNotNull()
3204             assertThat(eventsThatShouldNotTrigger).isFalse()
3205         }
3206 
3207         // UP (original + dynamically added modifiers)
3208         dispatchTouchEvent(
3209             ACTION_UP,
3210             box1LayoutCoordinates!!,
3211             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3212         )
3213         rule.runOnUiThread {
3214             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
3215             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
3216 
3217             // Verify Box 1 existing modifier events
3218             assertThat(preexistingModifierPress).isEqualTo(2)
3219             assertThat(preexistingModifierMove).isEqualTo(2)
3220             assertThat(preexistingModifierRelease).isEqualTo(2)
3221 
3222             // Verify Box 1 dynamically added modifier events
3223             assertThat(dynamicModifierPress).isEqualTo(1)
3224             assertThat(dynamicModifierMove).isEqualTo(1)
3225             assertThat(dynamicModifierRelease).isEqualTo(1)
3226 
3227             assertThat(pointerEvent).isNotNull()
3228             assertThat(eventsThatShouldNotTrigger).isFalse()
3229         }
3230     }
3231 
3232     /*
3233      * Tests how the input system handles TOUCH events going into a pointer input modifier node
3234      * when its parent's pointer input modifier is dynamically removed DURING an event stream, that
3235      * is, before the event stream ends.
3236      *
3237      * After any pointer input modifier node is removed, any existing event stream is cancelled,
3238      * and any follow up events from that stream are ignored. Any new event streams, will trigger
3239      * the appropriate nodes in the new tree.
3240      *
3241      * They key used is Unit.
3242      *
3243      * Specific events:
3244      *  1. UI Element (parent and child location): PRESS (touch)
3245      *  2. UI Element (parent and child location): MULTIPLE MOVE (touch)
3246      *  3. Dynamically remove parent pointer input modifier (between input event streams)
3247      *  4. System sends generated RELEASE to parent and child (this is not a user sent release)
3248      *  -- Events send in old input stream (Note: A new input stream starts with PRESS). ---
3249      *  5. UI Element (child only location [parent gone]): MULTIPLE MOVE (touch) - doesn't trigger
3250      *  6. UI Element (modifier 1 and 2): RELEASE (touch) - doesn't trigger
3251      *
3252      * TODO: If support added for dynamic modifier DURING event streams, modify test
3253      */
3254     @Test
3255     fun pointerInputEvents_removeParentInputModifierDuringStream_noFurtherEventsTriggerForStream() {
3256         // --> Arrange
3257         var parentBoxLayoutCoordinates: LayoutCoordinates? = null
3258         var childBoxLayoutCoordinates: LayoutCoordinates? = null
3259 
3260         val setUpFinishedLatch = CountDownLatch(2)
3261 
3262         var enableDynamicPointerInput by mutableStateOf(true)
3263 
3264         // Events for the Parent Box with dynamic pointer input modifier
3265         var parentBoxDynamicPointerInputScopeExecutionCount by mutableStateOf(0)
3266         var parentBoxDynamicModifierPress by mutableStateOf(0)
3267         var parentBoxDynamicModifierMove by mutableStateOf(0)
3268         var parentBoxDynamicModifierRelease by mutableStateOf(0)
3269 
3270         // Events for the child Box
3271         var childBoxPointerInputScopeExecutionCount by mutableStateOf(0)
3272         var childBoxModifierPress by mutableStateOf(0)
3273         var childBoxModifierMove by mutableStateOf(0)
3274         var childBoxModifierRelease by mutableStateOf(0)
3275 
3276         // All other events that should never be triggered in this test
3277         var eventsThatShouldNotTrigger by mutableStateOf(false)
3278 
3279         var pointerEvent: PointerEvent? by mutableStateOf(null)
3280 
3281         // Pointer Input Modifier that is toggled on/off based on passed value.
3282         fun Modifier.dynamicallyToggledPointerInput(
3283             enable: Boolean,
3284             pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
3285         ) =
3286             if (enable) {
3287                 pointerInput(Unit) {
3288                     ++parentBoxDynamicPointerInputScopeExecutionCount
3289 
3290                     // Reset pointer events when lambda is ran the first time
3291                     parentBoxDynamicModifierPress = 0
3292                     parentBoxDynamicModifierMove = 0
3293                     parentBoxDynamicModifierRelease = 0
3294 
3295                     awaitPointerEventScope {
3296                         while (true) {
3297                             pointerEventLambda(awaitPointerEvent())
3298                         }
3299                     }
3300                 }
3301             } else this
3302 
3303         // Setup UI
3304         rule.runOnUiThread {
3305             container.setContent {
3306                 // Parent Box
3307                 Box(
3308                     Modifier.background(Color.Green)
3309                         .size(400.dp)
3310                         .onGloballyPositioned {
3311                             parentBoxLayoutCoordinates = it
3312                             setUpFinishedLatch.countDown()
3313                         }
3314                         .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
3315                             when (it.type) {
3316                                 PointerEventType.Press -> {
3317                                     ++parentBoxDynamicModifierPress
3318                                 }
3319                                 PointerEventType.Move -> {
3320                                     ++parentBoxDynamicModifierMove
3321                                 }
3322                                 PointerEventType.Release -> {
3323                                     ++parentBoxDynamicModifierRelease
3324                                 }
3325                                 else -> {
3326                                     eventsThatShouldNotTrigger = true
3327                                 }
3328                             }
3329                         }
3330                 ) {
3331                     // Child box
3332                     Box(
3333                         Modifier.background(Color.Red)
3334                             .size(200.dp)
3335                             .onGloballyPositioned {
3336                                 childBoxLayoutCoordinates = it
3337                                 setUpFinishedLatch.countDown()
3338                             }
3339                             .pointerInput(Unit) {
3340                                 ++childBoxPointerInputScopeExecutionCount
3341                                 awaitPointerEventScope {
3342                                     while (true) {
3343                                         pointerEvent = awaitPointerEvent()
3344                                         when (pointerEvent!!.type) {
3345                                             PointerEventType.Press -> {
3346                                                 ++childBoxModifierPress
3347                                             }
3348                                             PointerEventType.Move -> {
3349                                                 ++childBoxModifierMove
3350                                             }
3351                                             PointerEventType.Release -> {
3352                                                 ++childBoxModifierRelease
3353                                             }
3354                                             else -> {
3355                                                 eventsThatShouldNotTrigger = true
3356                                             }
3357                                         }
3358                                     }
3359                                 }
3360                             }
3361                     ) {}
3362                 }
3363             }
3364         }
3365         // Ensure Arrange (setup) step is finished
3366         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
3367 
3368         // --> Act + Assert (interwoven)
3369         // DOWN (Starts Event Stream)
3370         dispatchTouchEvent(ACTION_DOWN, parentBoxLayoutCoordinates!!)
3371         rule.runOnUiThread {
3372             assertThat(childBoxPointerInputScopeExecutionCount).isEqualTo(1)
3373             assertThat(parentBoxDynamicPointerInputScopeExecutionCount).isEqualTo(1)
3374 
3375             assertThat(parentBoxDynamicModifierPress).isEqualTo(1)
3376             assertThat(parentBoxDynamicModifierMove).isEqualTo(0)
3377             assertThat(parentBoxDynamicModifierRelease).isEqualTo(0)
3378 
3379             assertThat(childBoxModifierPress).isEqualTo(1)
3380             assertThat(childBoxModifierMove).isEqualTo(0)
3381             assertThat(childBoxModifierRelease).isEqualTo(0)
3382 
3383             assertThat(pointerEvent).isNotNull()
3384             assertThat(eventsThatShouldNotTrigger).isFalse()
3385         }
3386 
3387         val moveAmount = 2f
3388         var moveYLocation = 0f
3389         var moveCount = 0
3390 
3391         (0..4).forEach { _ ->
3392             moveYLocation += moveAmount
3393             moveCount++
3394 
3395             dispatchTouchEvent(ACTION_MOVE, parentBoxLayoutCoordinates!!, Offset(0f, moveYLocation))
3396             rule.runOnUiThread {
3397                 assertThat(childBoxPointerInputScopeExecutionCount).isEqualTo(1)
3398                 assertThat(parentBoxDynamicPointerInputScopeExecutionCount).isEqualTo(1)
3399 
3400                 assertThat(parentBoxDynamicModifierPress).isEqualTo(1)
3401                 assertThat(parentBoxDynamicModifierMove).isEqualTo(moveCount)
3402                 assertThat(parentBoxDynamicModifierRelease).isEqualTo(0)
3403 
3404                 assertThat(childBoxModifierPress).isEqualTo(1)
3405                 assertThat(childBoxModifierMove).isEqualTo(moveCount)
3406                 assertThat(childBoxModifierRelease).isEqualTo(0)
3407 
3408                 assertThat(pointerEvent).isNotNull()
3409                 assertThat(eventsThatShouldNotTrigger).isFalse()
3410             }
3411         }
3412 
3413         // Remove top pointer modifier node
3414         enableDynamicPointerInput = false
3415         rule.waitForFutureFrame(2)
3416 
3417         // A generated Release was sent, but now all events moving forward do not trigger the
3418         // event listeners, since this is still the cancelled event stream.
3419         // (If we wanted to start a new event stream, we'd dispatch an ACTION_DOWN or hover enter).
3420 
3421         moveYLocation = 0f
3422         val oldMoveCount = moveCount
3423 
3424         (0..4).forEach { _ ->
3425             moveYLocation += moveAmount
3426             moveCount++
3427 
3428             dispatchTouchEvent(ACTION_MOVE, childBoxLayoutCoordinates!!, Offset(0f, moveYLocation))
3429             rule.runOnUiThread {
3430                 assertThat(childBoxPointerInputScopeExecutionCount).isEqualTo(1)
3431                 assertThat(parentBoxDynamicPointerInputScopeExecutionCount).isEqualTo(1)
3432 
3433                 assertThat(parentBoxDynamicModifierPress).isEqualTo(1)
3434                 assertThat(parentBoxDynamicModifierMove).isEqualTo(oldMoveCount)
3435                 assertThat(parentBoxDynamicModifierRelease).isEqualTo(1)
3436 
3437                 assertThat(childBoxModifierPress).isEqualTo(1)
3438                 assertThat(childBoxModifierMove).isEqualTo(oldMoveCount)
3439                 assertThat(childBoxModifierRelease).isEqualTo(1)
3440 
3441                 assertThat(pointerEvent).isNotNull()
3442                 assertThat(eventsThatShouldNotTrigger).isFalse()
3443             }
3444         }
3445 
3446         dispatchTouchEvent(
3447             ACTION_UP,
3448             childBoxLayoutCoordinates!!,
3449         )
3450         rule.runOnUiThread {
3451             assertThat(childBoxPointerInputScopeExecutionCount).isEqualTo(1)
3452             assertThat(parentBoxDynamicPointerInputScopeExecutionCount).isEqualTo(1)
3453 
3454             assertThat(parentBoxDynamicModifierPress).isEqualTo(1)
3455             assertThat(parentBoxDynamicModifierMove).isEqualTo(oldMoveCount)
3456             assertThat(parentBoxDynamicModifierRelease).isEqualTo(1)
3457 
3458             assertThat(childBoxModifierPress).isEqualTo(1)
3459             assertThat(childBoxModifierMove).isEqualTo(oldMoveCount)
3460             assertThat(childBoxModifierRelease).isEqualTo(1)
3461 
3462             assertThat(pointerEvent).isNotNull()
3463             assertThat(eventsThatShouldNotTrigger).isFalse()
3464         }
3465     }
3466 
3467     /*
3468      * Tests TOUCH events are triggered correctly when dynamically adding a pointer input
3469      * modifier BELOW an existing pointer input modifier.
3470      *
3471      * Note: The lambda for the existing pointer input modifier is NOT re-executed after the
3472      * dynamic one is added below it (since it doesn't impact it).
3473      *
3474      * Specific events:
3475      *  1. UI Element (modifier 1 only): PRESS (touch)
3476      *  2. UI Element (modifier 1 only): MOVE (touch)
3477      *  3. UI Element (modifier 1 only): RELEASE (touch)
3478      *  4. Dynamically add pointer input modifier below existing one (between input event streams)
3479      *  5. UI Element (modifier 1 and 2): PRESS (touch)
3480      *  6. UI Element (modifier 1 and 2): MOVE (touch)
3481      *  7. UI Element (modifier 1 and 2): RELEASE (touch)
3482      */
3483     @Test
3484     fun dynamicInputModifierWithKey_addsBelowExistingModifier_shouldTriggerInNewModifier() {
3485         // --> Arrange
3486         val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
3487         var box1LayoutCoordinates: LayoutCoordinates? = null
3488 
3489         val setUpFinishedLatch = CountDownLatch(1)
3490 
3491         var enableDynamicPointerInput by mutableStateOf(false)
3492 
3493         // Events for the lower modifier Box 1
3494         var originalPointerInputScopeExecutionCount by mutableStateOf(0)
3495         var preexistingModifierPress by mutableStateOf(0)
3496         var preexistingModifierMove by mutableStateOf(0)
3497         var preexistingModifierRelease by mutableStateOf(0)
3498 
3499         // Events for the dynamic upper modifier Box 1
3500         var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
3501         var dynamicModifierPress by mutableStateOf(0)
3502         var dynamicModifierMove by mutableStateOf(0)
3503         var dynamicModifierRelease by mutableStateOf(0)
3504 
3505         // All other events that should never be triggered in this test
3506         var eventsThatShouldNotTrigger by mutableStateOf(false)
3507 
3508         var pointerEvent: PointerEvent? by mutableStateOf(null)
3509 
3510         // Pointer Input Modifier that is toggled on/off based on passed value.
3511         fun Modifier.dynamicallyToggledPointerInput(
3512             enable: Boolean,
3513             pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
3514         ) =
3515             if (enable) {
3516                 pointerInput(pointerEventLambda) {
3517                     ++dynamicPointerInputScopeExecutionCount
3518 
3519                     // Reset pointer events when lambda is ran the first time
3520                     dynamicModifierPress = 0
3521                     dynamicModifierMove = 0
3522                     dynamicModifierRelease = 0
3523 
3524                     awaitPointerEventScope {
3525                         while (true) {
3526                             pointerEventLambda(awaitPointerEvent())
3527                         }
3528                     }
3529                 }
3530             } else this
3531 
3532         // Setup UI
3533         rule.runOnUiThread {
3534             container.setContent {
3535                 Box(
3536                     Modifier.size(200.dp)
3537                         .onGloballyPositioned {
3538                             box1LayoutCoordinates = it
3539                             setUpFinishedLatch.countDown()
3540                         }
3541                         .pointerInput(originalPointerInputModifierKey) {
3542                             ++originalPointerInputScopeExecutionCount
3543                             // Reset pointer events when lambda is ran the first time
3544                             preexistingModifierPress = 0
3545                             preexistingModifierMove = 0
3546                             preexistingModifierRelease = 0
3547 
3548                             awaitPointerEventScope {
3549                                 while (true) {
3550                                     pointerEvent = awaitPointerEvent()
3551                                     when (pointerEvent!!.type) {
3552                                         PointerEventType.Press -> {
3553                                             ++preexistingModifierPress
3554                                         }
3555                                         PointerEventType.Move -> {
3556                                             ++preexistingModifierMove
3557                                         }
3558                                         PointerEventType.Release -> {
3559                                             ++preexistingModifierRelease
3560                                         }
3561                                         else -> {
3562                                             eventsThatShouldNotTrigger = true
3563                                         }
3564                                     }
3565                                 }
3566                             }
3567                         }
3568                         .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
3569                             when (it.type) {
3570                                 PointerEventType.Press -> {
3571                                     ++dynamicModifierPress
3572                                 }
3573                                 PointerEventType.Move -> {
3574                                     ++dynamicModifierMove
3575                                 }
3576                                 PointerEventType.Release -> {
3577                                     ++dynamicModifierRelease
3578                                 }
3579                                 else -> {
3580                                     eventsThatShouldNotTrigger = true
3581                                 }
3582                             }
3583                         }
3584                 ) {}
3585             }
3586         }
3587         // Ensure Arrange (setup) step is finished
3588         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
3589 
3590         // --> Act + Assert (interwoven)
3591         // DOWN (original modifier only)
3592         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
3593         rule.runOnUiThread {
3594             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3595             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
3596 
3597             // Verify Box 1 existing modifier events
3598             assertThat(preexistingModifierPress).isEqualTo(1)
3599             assertThat(preexistingModifierMove).isEqualTo(0)
3600             assertThat(preexistingModifierRelease).isEqualTo(0)
3601 
3602             // Verify Box 1 dynamically added modifier events
3603             assertThat(dynamicModifierPress).isEqualTo(0)
3604             assertThat(dynamicModifierMove).isEqualTo(0)
3605             assertThat(dynamicModifierRelease).isEqualTo(0)
3606 
3607             assertThat(pointerEvent).isNotNull()
3608             assertThat(eventsThatShouldNotTrigger).isFalse()
3609         }
3610 
3611         // MOVE (original modifier only)
3612         dispatchTouchEvent(
3613             ACTION_MOVE,
3614             box1LayoutCoordinates!!,
3615             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3616         )
3617         rule.runOnUiThread {
3618             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3619             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
3620 
3621             // Verify Box 1 existing modifier events
3622             assertThat(preexistingModifierPress).isEqualTo(1)
3623             assertThat(preexistingModifierMove).isEqualTo(1)
3624             assertThat(preexistingModifierRelease).isEqualTo(0)
3625 
3626             // Verify Box 1 dynamically added modifier events
3627             assertThat(dynamicModifierPress).isEqualTo(0)
3628             assertThat(dynamicModifierMove).isEqualTo(0)
3629             assertThat(dynamicModifierRelease).isEqualTo(0)
3630 
3631             assertThat(pointerEvent).isNotNull()
3632             assertThat(eventsThatShouldNotTrigger).isFalse()
3633         }
3634 
3635         // UP (original modifier only)
3636         dispatchTouchEvent(
3637             ACTION_UP,
3638             box1LayoutCoordinates!!,
3639             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3640         )
3641         rule.runOnUiThread {
3642             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3643             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
3644 
3645             // Verify Box 1 existing modifier events
3646             assertThat(preexistingModifierPress).isEqualTo(1)
3647             assertThat(preexistingModifierMove).isEqualTo(1)
3648             assertThat(preexistingModifierRelease).isEqualTo(1)
3649 
3650             // Verify Box 1 dynamically added modifier events
3651             assertThat(dynamicModifierPress).isEqualTo(0)
3652             assertThat(dynamicModifierMove).isEqualTo(0)
3653             assertThat(dynamicModifierRelease).isEqualTo(0)
3654 
3655             assertThat(pointerEvent).isNotNull()
3656             assertThat(eventsThatShouldNotTrigger).isFalse()
3657         }
3658 
3659         enableDynamicPointerInput = true
3660         rule.waitForFutureFrame(2)
3661 
3662         // DOWN (original + dynamically added modifiers)
3663         dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
3664 
3665         rule.runOnUiThread {
3666             // Because the new pointer input modifier is added below the existing one, the existing
3667             // one doesn't change.
3668             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3669             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
3670 
3671             // Verify Box 1 existing modifier events
3672             assertThat(preexistingModifierPress).isEqualTo(2)
3673             assertThat(preexistingModifierMove).isEqualTo(1)
3674             assertThat(preexistingModifierRelease).isEqualTo(1)
3675 
3676             // Verify Box 1 dynamically added modifier events
3677             assertThat(dynamicModifierPress).isEqualTo(1)
3678             assertThat(dynamicModifierMove).isEqualTo(0)
3679             assertThat(dynamicModifierRelease).isEqualTo(0)
3680 
3681             assertThat(pointerEvent).isNotNull()
3682             assertThat(eventsThatShouldNotTrigger).isFalse()
3683         }
3684 
3685         // MOVE (original + dynamically added modifiers)
3686         dispatchTouchEvent(
3687             ACTION_MOVE,
3688             box1LayoutCoordinates!!,
3689             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3690         )
3691         rule.runOnUiThread {
3692             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3693             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
3694 
3695             // Verify Box 1 existing modifier events
3696             assertThat(preexistingModifierPress).isEqualTo(2)
3697             assertThat(preexistingModifierMove).isEqualTo(2)
3698             assertThat(preexistingModifierRelease).isEqualTo(1)
3699 
3700             // Verify Box 1 dynamically added modifier events
3701             assertThat(dynamicModifierPress).isEqualTo(1)
3702             assertThat(dynamicModifierMove).isEqualTo(1)
3703             assertThat(dynamicModifierRelease).isEqualTo(0)
3704 
3705             assertThat(pointerEvent).isNotNull()
3706             assertThat(eventsThatShouldNotTrigger).isFalse()
3707         }
3708 
3709         // UP (original + dynamically added modifiers)
3710         dispatchTouchEvent(
3711             ACTION_UP,
3712             box1LayoutCoordinates!!,
3713             Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
3714         )
3715         rule.runOnUiThread {
3716             assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
3717             assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
3718 
3719             // Verify Box 1 existing modifier events
3720             assertThat(preexistingModifierPress).isEqualTo(2)
3721             assertThat(preexistingModifierMove).isEqualTo(2)
3722             assertThat(preexistingModifierRelease).isEqualTo(2)
3723 
3724             // Verify Box 1 dynamically added modifier events
3725             assertThat(dynamicModifierPress).isEqualTo(1)
3726             assertThat(dynamicModifierMove).isEqualTo(1)
3727             assertThat(dynamicModifierRelease).isEqualTo(1)
3728 
3729             assertThat(pointerEvent).isNotNull()
3730             assertThat(eventsThatShouldNotTrigger).isFalse()
3731         }
3732     }
3733 
3734     /*
3735      * Tests a full mouse event cycle from a press and release.
3736      *
3737      * Important Note: The pointer id should stay the same throughout all these events (part of the
3738      * test).
3739      *
3740      * Specific MotionEvents:
3741      *  1. UI Element 1: ENTER (hover enter [mouse])
3742      *  2. UI Element 1: EXIT (hover exit [mouse]) - Doesn't trigger Compose PointerEvent
3743      *  3. UI Element 1: PRESS (mouse)
3744      *  4. UI Element 1: ACTION_BUTTON_PRESS (mouse)
3745      *  5. UI Element 1: ACTION_BUTTON_RELEASE (mouse)
3746      *  6. UI Element 1: RELEASE (mouse)
3747      *  7. UI Element 1: ENTER (hover enter [mouse]) - Doesn't trigger Compose PointerEvent
3748      *  8. UI Element 1: EXIT (hover enter [mouse])
3749      *
3750      * Should NOT trigger any additional events (like an extra press or exit)!
3751      */
3752     @Test
3753     fun mouseEventsAndPointerIds_completeMouseEventCycle_pointerIdsShouldMatchAcrossAllEvents() {
3754         // --> Arrange
3755         var box1LayoutCoordinates: LayoutCoordinates? = null
3756 
3757         val setUpFinishedLatch = CountDownLatch(1)
3758 
3759         // Events for Box
3760         var hoverEventCount = 0
3761         var hoverExitCount = 0
3762         var downCount = 0
3763         // unknownCount covers both button action press and release from Android system for a
3764         // mouse. These events happen between the normal press and release events.
3765         var unknownCount = 0
3766         var upCount = 0
3767 
3768         // We want to assert that each updated pointer id matches the original pointer id that
3769         // starts the sequence of MotionEvents.
3770         var originalPointerId = -1L
3771         var box1PointerId = -1L
3772 
3773         // All other events that should never be triggered in this test
3774         var eventsThatShouldNotTrigger = false
3775 
3776         var pointerEvent: PointerEvent? = null
3777 
3778         rule.runOnUiThread {
3779             container.setContent {
3780                 Box(
3781                     Modifier.size(50.dp)
3782                         .onGloballyPositioned {
3783                             box1LayoutCoordinates = it
3784                             setUpFinishedLatch.countDown()
3785                         }
3786                         .pointerInput(Unit) {
3787                             awaitPointerEventScope {
3788                                 while (true) {
3789                                     pointerEvent = awaitPointerEvent()
3790 
3791                                     if (originalPointerId < 0) {
3792                                         originalPointerId = pointerEvent!!.changes[0].id.value
3793                                     }
3794 
3795                                     box1PointerId = pointerEvent!!.changes[0].id.value
3796 
3797                                     when (pointerEvent!!.type) {
3798                                         PointerEventType.Enter -> {
3799                                             ++hoverEventCount
3800                                         }
3801                                         PointerEventType.Press -> {
3802                                             ++downCount
3803                                         }
3804                                         PointerEventType.Release -> {
3805                                             ++upCount
3806                                         }
3807                                         PointerEventType.Exit -> {
3808                                             ++hoverExitCount
3809                                         }
3810                                         PointerEventType.Unknown -> {
3811                                             ++unknownCount
3812                                         }
3813                                         else -> {
3814                                             eventsThatShouldNotTrigger = true
3815                                         }
3816                                     }
3817                                 }
3818                             }
3819                         }
3820                 ) {}
3821             }
3822         }
3823         // Ensure Arrange (setup) step is finished
3824         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
3825 
3826         // --> Act + Assert (interwoven)
3827         dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
3828         rule.runOnUiThread {
3829             // Verify Box 1 events and pointer id
3830             assertThat(originalPointerId).isEqualTo(box1PointerId)
3831             assertThat(hoverEventCount).isEqualTo(1)
3832             assertThat(hoverExitCount).isEqualTo(0)
3833             assertThat(downCount).isEqualTo(0)
3834             assertThat(unknownCount).isEqualTo(0)
3835             assertThat(upCount).isEqualTo(0)
3836 
3837             assertThat(pointerEvent).isNotNull()
3838             assertThat(eventsThatShouldNotTrigger).isFalse()
3839             assertHoverEvent(pointerEvent!!, isEnter = true)
3840         }
3841 
3842         pointerEvent = null // Reset before each event
3843 
3844         // This will be interpreted as a synthetic event triggered by an ACTION_DOWN because we
3845         // don't wait several frames before triggering the ACTION_DOWN. Thus, no hover exit is
3846         // triggered.
3847         dispatchMouseEvent(ACTION_HOVER_EXIT, box1LayoutCoordinates!!)
3848         dispatchMouseEvent(ACTION_DOWN, box1LayoutCoordinates!!)
3849 
3850         rule.runOnUiThread {
3851             assertThat(originalPointerId).isEqualTo(box1PointerId)
3852             assertThat(hoverEventCount).isEqualTo(1)
3853             assertThat(hoverExitCount).isEqualTo(0)
3854             assertThat(downCount).isEqualTo(1)
3855             assertThat(unknownCount).isEqualTo(0)
3856             assertThat(upCount).isEqualTo(0)
3857 
3858             assertThat(pointerEvent).isNotNull()
3859             assertThat(eventsThatShouldNotTrigger).isFalse()
3860         }
3861 
3862         pointerEvent = null // Reset before each event
3863         dispatchMouseEvent(ACTION_BUTTON_PRESS, box1LayoutCoordinates!!)
3864         rule.runOnUiThread {
3865             // Verify Box 1 events
3866             assertThat(originalPointerId).isEqualTo(box1PointerId)
3867             assertThat(hoverEventCount).isEqualTo(1)
3868             assertThat(hoverExitCount).isEqualTo(0)
3869             assertThat(downCount).isEqualTo(1)
3870             // unknownCount covers both button action press and release from Android system for a
3871             // mouse. These events happen between the normal press and release events.
3872             assertThat(unknownCount).isEqualTo(1)
3873             assertThat(upCount).isEqualTo(0)
3874 
3875             assertThat(pointerEvent).isNotNull()
3876             assertThat(eventsThatShouldNotTrigger).isFalse()
3877         }
3878 
3879         pointerEvent = null // Reset before each event
3880         dispatchMouseEvent(ACTION_BUTTON_RELEASE, box1LayoutCoordinates!!)
3881         rule.runOnUiThread {
3882             assertThat(originalPointerId).isEqualTo(box1PointerId)
3883             assertThat(hoverEventCount).isEqualTo(1)
3884             assertThat(hoverExitCount).isEqualTo(0)
3885             assertThat(downCount).isEqualTo(1)
3886             // unknownCount covers both button action press and release from Android system for a
3887             // mouse. These events happen between the normal press and release events.
3888             assertThat(unknownCount).isEqualTo(2)
3889             assertThat(upCount).isEqualTo(0)
3890 
3891             assertThat(pointerEvent).isNotNull()
3892             assertThat(eventsThatShouldNotTrigger).isFalse()
3893         }
3894 
3895         pointerEvent = null // Reset before each event
3896         dispatchMouseEvent(ACTION_UP, box1LayoutCoordinates!!)
3897         // Compose already considered us as ENTERING the UI Element, so we don't need to trigger
3898         // it again. (This is a synthetic hover enter anyway sent from platform with UP.)
3899         dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
3900 
3901         rule.runOnUiThread {
3902             assertThat(originalPointerId).isEqualTo(box1PointerId)
3903             assertThat(hoverEventCount).isEqualTo(1)
3904             assertThat(hoverExitCount).isEqualTo(0)
3905             assertThat(downCount).isEqualTo(1)
3906             assertThat(unknownCount).isEqualTo(2)
3907             assertThat(upCount).isEqualTo(1)
3908 
3909             assertThat(pointerEvent).isNotNull()
3910             assertThat(eventsThatShouldNotTrigger).isFalse()
3911         }
3912 
3913         pointerEvent = null // Reset before each event
3914         dispatchMouseEvent(ACTION_HOVER_EXIT, box1LayoutCoordinates!!)
3915 
3916         // Wait enough time for timeout on hover exit to trigger
3917         rule.waitForFutureFrame(2)
3918 
3919         rule.runOnUiThread {
3920             assertThat(originalPointerId).isEqualTo(box1PointerId)
3921             assertThat(hoverEventCount).isEqualTo(1)
3922             assertThat(hoverExitCount).isEqualTo(1)
3923             assertThat(downCount).isEqualTo(1)
3924             assertThat(unknownCount).isEqualTo(2)
3925             assertThat(upCount).isEqualTo(1)
3926 
3927             assertThat(pointerEvent).isNotNull()
3928             assertThat(eventsThatShouldNotTrigger).isFalse()
3929         }
3930     }
3931 
3932     /*
3933      * Tests an ACTION_HOVER_EXIT MotionEvent is ignored in Compose when it proceeds an
3934      * ACTION_DOWN MotionEvent (in a measure of milliseconds only).
3935      *
3936      * The event order of MotionEvents:
3937      *   - Hover enter on box 1
3938      *   - Loop 10 times:
3939      *     - Hover enter on box 1
3940      *     - Down on box 1
3941      *     - Up on box 1
3942      */
3943     @Test
3944     fun hoverExitBeforeDownMotionEvent_shortDelayBetweenMotionEvents_shouldNotTriggerHoverExit() {
3945         // --> Arrange
3946         var box1LayoutCoordinates: LayoutCoordinates? = null
3947 
3948         val setUpFinishedLatch = CountDownLatch(4)
3949 
3950         // Events for Box 1
3951         var enterBox1 = false
3952         var pressBox1 = false
3953 
3954         // All other events that should never be triggered in this test
3955         var eventsThatShouldNotTrigger = false
3956 
3957         var pointerEvent: PointerEvent? = null
3958 
3959         rule.runOnUiThread {
3960             container.setContent {
3961                 Column(
3962                     Modifier.fillMaxSize().onGloballyPositioned { setUpFinishedLatch.countDown() }
3963                 ) {
3964                     // Box 1
3965                     Box(
3966                         Modifier.size(50.dp)
3967                             .onGloballyPositioned {
3968                                 box1LayoutCoordinates = it
3969                                 setUpFinishedLatch.countDown()
3970                             }
3971                             .pointerInput(Unit) {
3972                                 awaitPointerEventScope {
3973                                     while (true) {
3974                                         pointerEvent = awaitPointerEvent()
3975 
3976                                         when (pointerEvent!!.type) {
3977                                             PointerEventType.Enter -> {
3978                                                 enterBox1 = true
3979                                             }
3980                                             PointerEventType.Exit -> {
3981                                                 enterBox1 = false
3982                                             }
3983                                             PointerEventType.Press -> {
3984                                                 pressBox1 = true
3985                                             }
3986                                             PointerEventType.Release -> {
3987                                                 pressBox1 = false
3988                                             }
3989                                             else -> {
3990                                                 eventsThatShouldNotTrigger = true
3991                                             }
3992                                         }
3993                                     }
3994                                 }
3995                             }
3996                     ) {}
3997 
3998                     // Box 2
3999                     Box(
4000                         Modifier.size(50.dp)
4001                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
4002                             .pointerInput(Unit) {
4003                                 awaitPointerEventScope {
4004                                     while (true) {
4005                                         pointerEvent = awaitPointerEvent()
4006                                         // Should never do anything with this UI element.
4007                                         eventsThatShouldNotTrigger = true
4008                                     }
4009                                 }
4010                             }
4011                     ) {}
4012 
4013                     // Box 3
4014                     Box(
4015                         Modifier.size(50.dp)
4016                             .onGloballyPositioned { setUpFinishedLatch.countDown() }
4017                             .pointerInput(Unit) {
4018                                 awaitPointerEventScope {
4019                                     while (true) {
4020                                         pointerEvent = awaitPointerEvent()
4021                                         // Should never do anything with this UI element.
4022                                         eventsThatShouldNotTrigger = true
4023                                     }
4024                                 }
4025                             }
4026                     ) {}
4027                 }
4028             }
4029         }
4030         // Ensure Arrange (setup) step is finished
4031         assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
4032 
4033         // --> Act + Assert (interwoven)
4034         // Hover Enter on Box 1
4035         dispatchMouseEvent(ACTION_HOVER_ENTER, box1LayoutCoordinates!!)
4036         rule.runOnUiThread {
4037             assertThat(enterBox1).isTrue()
4038             assertThat(pointerEvent).isNotNull()
4039             assertThat(eventsThatShouldNotTrigger).isFalse()
4040             assertHoverEvent(pointerEvent!!, isEnter = true)
4041         }
4042 
4043         pointerEvent = null // Reset before each event
4044 
4045         for (index in 0 until 10) {
4046             // We do not use dispatchMouseEvent() to dispatch the following two events, because the
4047             // actions need to be executed in immediate succession.
4048             rule.runOnUiThread {
4049                 val root = box1LayoutCoordinates!!.findRootCoordinates()
4050                 val pos = root.localPositionOf(box1LayoutCoordinates!!, Offset.Zero)
4051 
4052                 // Exit on Box 1 right before action down. This happens normally on devices, so we
4053                 // are recreating it here. However, Compose ignores the exit if it is right before
4054                 // a down (right before meaning within a couple milliseconds). We verify that it
4055                 // did in fact ignore this exit.
4056                 val exitMotionEvent =
4057                     MotionEvent(
4058                         0,
4059                         ACTION_HOVER_EXIT,
4060                         1,
4061                         0,
4062                         arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_MOUSE }),
4063                         arrayOf(PointerCoords(pos.x, pos.y, Offset.Zero.x, Offset.Zero.y))
4064                     )
4065 
4066                 // Press on Box 1
4067                 val downMotionEvent =
4068                     MotionEvent(
4069                         0,
4070                         ACTION_DOWN,
4071                         1,
4072                         0,
4073                         arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_MOUSE }),
4074                         arrayOf(PointerCoords(pos.x, pos.y, Offset.Zero.x, Offset.Zero.y))
4075                     )
4076 
4077                 val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
4078 
4079                 // Execute events
4080                 androidComposeView.dispatchHoverEvent(exitMotionEvent)
4081                 androidComposeView.dispatchTouchEvent(downMotionEvent)
4082             }
4083 
4084             // In Compose, a hover exit MotionEvent is ignored if it is quickly followed
4085             // by a press.
4086             rule.runOnUiThread {
4087                 assertThat(enterBox1).isTrue()
4088                 assertThat(pressBox1).isTrue()
4089                 assertThat(eventsThatShouldNotTrigger).isFalse()
4090                 assertThat(pointerEvent).isNotNull()
4091             }
4092 
4093             // Release on Box 1
4094             pointerEvent = null // Reset before each event
4095             dispatchTouchEvent(ACTION_UP, box1LayoutCoordinates!!)
4096 
4097             rule.runOnUiThread {
4098                 assertThat(enterBox1).isTrue()
4099                 assertThat(pressBox1).isFalse()
4100                 assertThat(eventsThatShouldNotTrigger).isFalse()
4101                 assertThat(pointerEvent).isNotNull()
4102             }
4103         }
4104 
4105         rule.runOnUiThread { assertThat(eventsThatShouldNotTrigger).isFalse() }
4106     }
4107 
4108     @Test
4109     fun dispatchHoverMove() {
4110         var layoutCoordinates: LayoutCoordinates? = null
4111         var layoutCoordinates2: LayoutCoordinates? = null
4112         val latch = CountDownLatch(1)
4113         val eventLatch = CountDownLatch(1)
4114         var anyOtherEvent = false
4115 
4116         var move1 = false
4117         var move2 = false
4118         var move3 = false
4119 
4120         var enter: PointerEvent? = null
4121         var move: PointerEvent? = null
4122         var exit: PointerEvent? = null
4123 
4124         rule.runOnUiThread {
4125             container.setContent {
4126                 Box(
4127                     Modifier.fillMaxSize()
4128                         .onGloballyPositioned {
4129                             layoutCoordinates = it
4130                             latch.countDown()
4131                         }
4132                         .pointerInput(Unit) {
4133                             awaitPointerEventScope {
4134                                 awaitPointerEvent() // enter
4135                                 assertHoverEvent(awaitPointerEvent()) // move
4136                                 move1 = true
4137                                 assertHoverEvent(awaitPointerEvent()) // move
4138                                 move2 = true
4139                                 assertHoverEvent(awaitPointerEvent()) // move
4140                                 move3 = true
4141                                 awaitPointerEvent() // exit
4142                                 eventLatch.countDown()
4143                             }
4144                         }
4145                 ) {
4146                     Box(
4147                         Modifier.align(Alignment.Center)
4148                             .size(50.dp)
4149                             .onGloballyPositioned { layoutCoordinates2 = it }
4150                             .pointerInput(Unit) {
4151                                 awaitPointerEventScope {
4152                                     enter = awaitPointerEvent()
4153                                     move = awaitPointerEvent()
4154                                     exit = awaitPointerEvent()
4155                                     awaitPointerEvent()
4156                                     anyOtherEvent = true
4157                                 }
4158                             }
4159                     )
4160                 }
4161             }
4162         }
4163         assertTrue(latch.await(1, TimeUnit.SECONDS))
4164         // Enter outer Box
4165         dispatchMouseEvent(ACTION_HOVER_ENTER, layoutCoordinates!!)
4166 
4167         // Move to inner Box
4168         dispatchMouseEvent(ACTION_HOVER_MOVE, layoutCoordinates2!!)
4169         rule.runOnUiThread {
4170             assertThat(move1).isTrue()
4171             assertThat(enter).isNotNull()
4172             assertHoverEvent(enter!!, isEnter = true)
4173         }
4174 
4175         // Move within inner Box
4176         dispatchMouseEvent(ACTION_HOVER_MOVE, layoutCoordinates2!!, Offset(1f, 1f))
4177         rule.runOnUiThread {
4178             assertThat(move2).isTrue()
4179             assertThat(move).isNotNull()
4180             assertHoverEvent(move!!)
4181         }
4182 
4183         // Move to outer Box
4184         dispatchMouseEvent(ACTION_HOVER_MOVE, layoutCoordinates!!)
4185         rule.runOnUiThread {
4186             assertThat(move3).isTrue()
4187             assertThat(exit).isNotNull()
4188             assertHoverEvent(exit!!, isExit = true)
4189         }
4190 
4191         // Leave outer Box
4192         dispatchMouseEvent(ACTION_HOVER_EXIT, layoutCoordinates!!)
4193 
4194         rule.runOnUiThread { assertThat(anyOtherEvent).isFalse() }
4195         assertTrue(eventLatch.await(1, TimeUnit.SECONDS))
4196     }
4197 
4198     @Test
4199     fun dispatchScroll() {
4200         var layoutCoordinates: LayoutCoordinates? = null
4201         val latch = CountDownLatch(1)
4202         val events = mutableListOf<PointerEvent>()
4203         val scrollDelta = Offset(0.35f, 0.65f)
4204         rule.runOnUiThread {
4205             container.setContent {
4206                 Box(
4207                     Modifier.fillMaxSize()
4208                         .onGloballyPositioned {
4209                             layoutCoordinates = it
4210                             latch.countDown()
4211                         }
4212                         .pointerInput(Unit) {
4213                             awaitPointerEventScope {
4214                                 while (true) {
4215                                     val event = awaitPointerEvent()
4216                                     event.changes[0].consume()
4217                                     events += event
4218                                 }
4219                             }
4220                         }
4221                 )
4222             }
4223         }
4224         assertTrue(latch.await(1, TimeUnit.SECONDS))
4225         dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = scrollDelta)
4226         rule.runOnUiThread {
4227             assertThat(events).hasSize(2) // synthetic enter and scroll
4228             assertHoverEvent(events[0], isEnter = true)
4229             assertScrollEvent(events[1], scrollExpected = scrollDelta)
4230         }
4231     }
4232 
4233     @Test
4234     fun dispatchScroll_whenButtonPressed() {
4235         var layoutCoordinates: LayoutCoordinates? = null
4236         val latch = CountDownLatch(1)
4237         val events = mutableListOf<PointerEvent>()
4238         val scrollDelta = Offset(0.35f, 0.65f)
4239         rule.runOnUiThread {
4240             container.setContent {
4241                 Box(
4242                     Modifier.fillMaxSize()
4243                         .onGloballyPositioned {
4244                             layoutCoordinates = it
4245                             latch.countDown()
4246                         }
4247                         .pointerInput(Unit) {
4248                             awaitPointerEventScope {
4249                                 while (true) {
4250                                     val event = awaitPointerEvent()
4251                                     event.changes[0].consume()
4252                                     events += event
4253                                 }
4254                             }
4255                         }
4256                 )
4257             }
4258         }
4259         assertTrue(latch.await(1, TimeUnit.SECONDS))
4260         // press the button first before scroll
4261         dispatchMouseEvent(ACTION_DOWN, layoutCoordinates!!)
4262         dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = scrollDelta)
4263         rule.runOnUiThread {
4264             assertThat(events).hasSize(3) // synthetic enter, button down, scroll
4265             assertHoverEvent(events[0], isEnter = true)
4266             assert(events[1].changes.fastAll { it.changedToDownIgnoreConsumed() })
4267             assertScrollEvent(events[2], scrollExpected = scrollDelta)
4268         }
4269     }
4270 
4271     @Test
4272     fun dispatchScroll_batch() {
4273         var layoutCoordinates: LayoutCoordinates? = null
4274         val latch = CountDownLatch(1)
4275         val events = mutableListOf<PointerEvent>()
4276         val scrollDelta1 = Offset(0.32f, -0.75f)
4277         val scrollDelta2 = Offset(0.14f, 0.35f)
4278         val scrollDelta3 = Offset(-0.30f, -0.12f)
4279         val scrollDelta4 = Offset(-0.05f, 0.68f)
4280         rule.runOnUiThread {
4281             container.setContent {
4282                 Box(
4283                     Modifier.fillMaxSize()
4284                         .onGloballyPositioned {
4285                             layoutCoordinates = it
4286                             latch.countDown()
4287                         }
4288                         .pointerInput(Unit) {
4289                             awaitPointerEventScope {
4290                                 while (true) {
4291                                     val event = awaitPointerEvent()
4292                                     event.changes[0].consume()
4293                                     events += event
4294                                 }
4295                             }
4296                         }
4297                 )
4298             }
4299         }
4300         assertTrue(latch.await(1, TimeUnit.SECONDS))
4301         listOf(scrollDelta1, scrollDelta2, scrollDelta3, scrollDelta4).fastForEach {
4302             dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = it)
4303         }
4304         rule.runOnUiThread {
4305             assertThat(events).hasSize(5) // 4 + synthetic enter
4306             assertHoverEvent(events[0], isEnter = true)
4307             assertScrollEvent(events[1], scrollExpected = scrollDelta1)
4308             assertScrollEvent(events[2], scrollExpected = scrollDelta2)
4309             assertScrollEvent(events[3], scrollExpected = scrollDelta3)
4310             assertScrollEvent(events[4], scrollExpected = scrollDelta4)
4311         }
4312     }
4313 
4314     @Test
4315     fun mouseScroll_ignoredAsDownEvent() {
4316         var layoutCoordinates: LayoutCoordinates? = null
4317         val latch = CountDownLatch(1)
4318         val events = mutableListOf<PointerEvent>()
4319         val scrollDelta = Offset(0.35f, 0.65f)
4320         rule.runOnUiThread {
4321             container.setContent {
4322                 Box(
4323                     Modifier.fillMaxSize()
4324                         .onGloballyPositioned {
4325                             layoutCoordinates = it
4326                             latch.countDown()
4327                         }
4328                         .pointerInput(Unit) {
4329                             awaitPointerEventScope {
4330                                 while (true) {
4331                                     val event = awaitPointerEvent()
4332                                     event.changes[0].consume()
4333                                     events += event
4334                                 }
4335                             }
4336                         }
4337                 )
4338             }
4339         }
4340         assertTrue(latch.await(1, TimeUnit.SECONDS))
4341         dispatchMouseEvent(ACTION_SCROLL, layoutCoordinates!!, scrollDelta = scrollDelta)
4342         rule.runOnUiThread {
4343             assertThat(events).hasSize(2) // hover enter + scroll
4344             assertThat(events[1].changes).isNotEmpty()
4345             assertThat(events[1].changes[0].changedToDown()).isFalse()
4346         }
4347     }
4348 
4349     @Test
4350     fun mousePress_ignoresHoverExitOnPress() {
4351         lateinit var layoutCoordinates: LayoutCoordinates
4352         val latch = CountDownLatch(1)
4353         val events = mutableListOf<PointerEvent>()
4354         rule.runOnUiThread {
4355             container.setContent {
4356                 Box(
4357                     Modifier.fillMaxSize()
4358                         .onGloballyPositioned {
4359                             layoutCoordinates = it
4360                             latch.countDown()
4361                         }
4362                         .pointerInput(Unit) {
4363                             awaitPointerEventScope {
4364                                 while (true) {
4365                                     val event = awaitPointerEvent()
4366                                     event.changes[0].consume()
4367                                     events += event
4368                                 }
4369                             }
4370                         }
4371                 )
4372             }
4373         }
4374         assertTrue(latch.await(1, TimeUnit.SECONDS))
4375 
4376         rule.runOnUiThread {
4377             val root = layoutCoordinates.findRootCoordinates()
4378             val pos = root.localPositionOf(layoutCoordinates, Offset.Zero)
4379             val pointerCoords = PointerCoords(pos.x, pos.y)
4380             val pointerProperties = PointerProperties(0).apply { toolType = TOOL_TYPE_MOUSE }
4381             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
4382 
4383             val hoverExitEvent =
4384                 MotionEvent(
4385                     eventTime = 0,
4386                     action = ACTION_HOVER_EXIT,
4387                     numPointers = 1,
4388                     actionIndex = 0,
4389                     pointerProperties = arrayOf(pointerProperties),
4390                     pointerCoords = arrayOf(pointerCoords),
4391                     buttonState = 0,
4392                 )
4393             androidComposeView.dispatchHoverEvent(hoverExitEvent)
4394 
4395             val downEvent =
4396                 MotionEvent(
4397                     eventTime = 0,
4398                     action = ACTION_DOWN,
4399                     numPointers = 1,
4400                     actionIndex = 0,
4401                     pointerProperties = arrayOf(pointerProperties),
4402                     pointerCoords = arrayOf(pointerCoords),
4403                 )
4404             androidComposeView.dispatchTouchEvent(downEvent)
4405         }
4406 
4407         rule.runOnUiThread {
4408             assertThat(events).hasSize(1)
4409             assertThat(events.single().type).isEqualTo(PointerEventType.Press)
4410         }
4411     }
4412 
4413     @Test
4414     fun hoverEnterPressExitEnterExitRelease() {
4415         var outerCoordinates: LayoutCoordinates? = null
4416         var innerCoordinates: LayoutCoordinates? = null
4417         val latch = CountDownLatch(1)
4418         val eventLog = mutableListOf<PointerEvent>()
4419         rule.runOnUiThread {
4420             container.setContent {
4421                 Box(
4422                     Modifier.fillMaxSize().onGloballyPositioned {
4423                         outerCoordinates = it
4424                         latch.countDown()
4425                     }
4426                 ) {
4427                     Box(
4428                         Modifier.align(Alignment.Center)
4429                             .size(50.dp)
4430                             .pointerInput(Unit) {
4431                                 awaitPointerEventScope {
4432                                     while (true) {
4433                                         val event = awaitPointerEvent()
4434                                         event.changes[0].consume()
4435                                         eventLog += event
4436                                     }
4437                                 }
4438                             }
4439                             .onGloballyPositioned { innerCoordinates = it }
4440                     )
4441                 }
4442             }
4443         }
4444         assertTrue(latch.await(1, TimeUnit.SECONDS))
4445         dispatchMouseEvent(ACTION_HOVER_ENTER, outerCoordinates!!)
4446         dispatchMouseEvent(ACTION_HOVER_MOVE, innerCoordinates!!)
4447         dispatchMouseEvent(ACTION_DOWN, innerCoordinates!!)
4448         dispatchMouseEvent(ACTION_MOVE, outerCoordinates!!)
4449         dispatchMouseEvent(ACTION_MOVE, innerCoordinates!!)
4450         dispatchMouseEvent(ACTION_MOVE, outerCoordinates!!)
4451         dispatchMouseEvent(ACTION_UP, outerCoordinates!!)
4452         rule.runOnUiThread {
4453             assertThat(eventLog).hasSize(6)
4454             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4455             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
4456             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Exit)
4457             assertThat(eventLog[3].type).isEqualTo(PointerEventType.Enter)
4458             assertThat(eventLog[4].type).isEqualTo(PointerEventType.Exit)
4459             assertThat(eventLog[5].type).isEqualTo(PointerEventType.Release)
4460         }
4461     }
4462 
4463     @Test
4464     fun hoverPressEnterRelease() {
4465         var missCoordinates: LayoutCoordinates? = null
4466         var hitCoordinates: LayoutCoordinates? = null
4467         val latch = CountDownLatch(1)
4468         val eventLog = mutableListOf<PointerEvent>()
4469         rule.runOnUiThread {
4470             container.setContent {
4471                 Box(Modifier.fillMaxSize()) {
4472                     Box(
4473                         Modifier.align(AbsoluteAlignment.TopLeft)
4474                             .size(50.dp)
4475                             .pointerInput(Unit) {
4476                                 awaitPointerEventScope {
4477                                     while (true) {
4478                                         awaitPointerEvent()
4479                                     }
4480                                 }
4481                             }
4482                             .onGloballyPositioned {
4483                                 missCoordinates = it
4484                                 latch.countDown()
4485                             }
4486                     )
4487                     Box(
4488                         Modifier.align(AbsoluteAlignment.BottomRight)
4489                             .size(50.dp)
4490                             .pointerInput(Unit) {
4491                                 awaitPointerEventScope {
4492                                     while (true) {
4493                                         val event = awaitPointerEvent()
4494                                         event.changes[0].consume()
4495                                         eventLog += event
4496                                     }
4497                                 }
4498                             }
4499                             .onGloballyPositioned { hitCoordinates = it }
4500                     )
4501                 }
4502             }
4503         }
4504         assertTrue(latch.await(1, TimeUnit.SECONDS))
4505         dispatchMouseEvent(ACTION_HOVER_ENTER, missCoordinates!!)
4506         dispatchMouseEvent(ACTION_HOVER_EXIT, missCoordinates!!)
4507         dispatchMouseEvent(ACTION_DOWN, missCoordinates!!)
4508         dispatchMouseEvent(ACTION_MOVE, hitCoordinates!!)
4509         dispatchMouseEvent(ACTION_UP, hitCoordinates!!)
4510         dispatchMouseEvent(ACTION_HOVER_ENTER, hitCoordinates!!)
4511         rule.runOnUiThread {
4512             assertThat(eventLog).hasSize(1)
4513             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4514         }
4515     }
4516 
4517     @Test
4518     fun pressInsideExitWindow() {
4519         var innerCoordinates: LayoutCoordinates? = null
4520         val latch = CountDownLatch(1)
4521         val eventLog = mutableListOf<PointerEvent>()
4522         rule.runOnUiThread {
4523             container.setContent {
4524                 Box(Modifier.fillMaxSize()) {
4525                     Box(
4526                         Modifier.align(Alignment.BottomCenter)
4527                             .size(50.dp)
4528                             .graphicsLayer { translationY = 25.dp.roundToPx().toFloat() }
4529                             .pointerInput(Unit) {
4530                                 awaitPointerEventScope {
4531                                     while (true) {
4532                                         val event = awaitPointerEvent()
4533                                         event.changes[0].consume()
4534                                         eventLog += event
4535                                     }
4536                                 }
4537                             }
4538                             .onGloballyPositioned {
4539                                 innerCoordinates = it
4540                                 latch.countDown()
4541                             }
4542                     )
4543                 }
4544             }
4545         }
4546         assertTrue(latch.await(1, TimeUnit.SECONDS))
4547         val coords = innerCoordinates!!
4548         dispatchMouseEvent(ACTION_HOVER_ENTER, coords)
4549         dispatchMouseEvent(ACTION_DOWN, coords)
4550         dispatchMouseEvent(ACTION_MOVE, coords, Offset(0f, coords.size.height / 2 - 1f))
4551         dispatchMouseEvent(ACTION_MOVE, coords, Offset(0f, coords.size.height - 1f))
4552         dispatchMouseEvent(ACTION_UP, coords, Offset(0f, coords.size.height - 1f))
4553         rule.runOnUiThread {
4554             assertThat(eventLog).hasSize(5)
4555             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4556             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
4557             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Move)
4558             assertThat(eventLog[3].type).isEqualTo(PointerEventType.Exit)
4559             assertThat(eventLog[4].type).isEqualTo(PointerEventType.Release)
4560         }
4561     }
4562 
4563     @Test
4564     fun pressInsideClippedContent() {
4565         var innerCoordinates: LayoutCoordinates? = null
4566         val latch = CountDownLatch(1)
4567         val eventLog = mutableListOf<PointerEvent>()
4568         rule.runOnUiThread {
4569             container.setContent {
4570                 Box(Modifier.fillMaxSize()) {
4571                     Box(Modifier.align(Alignment.TopCenter).requiredSize(50.dp).clipToBounds()) {
4572                         Box(
4573                             Modifier.requiredSize(50.dp)
4574                                 .graphicsLayer { translationY = 25.dp.roundToPx().toFloat() }
4575                                 .pointerInput(Unit) {
4576                                     awaitPointerEventScope {
4577                                         while (true) {
4578                                             val event = awaitPointerEvent()
4579                                             event.changes[0].consume()
4580                                             eventLog += event
4581                                         }
4582                                     }
4583                                 }
4584                                 .onGloballyPositioned {
4585                                     innerCoordinates = it
4586                                     latch.countDown()
4587                                 }
4588                         )
4589                     }
4590                 }
4591             }
4592         }
4593         assertTrue(latch.await(1, TimeUnit.SECONDS))
4594         val coords = innerCoordinates!!
4595         dispatchMouseEvent(ACTION_HOVER_ENTER, coords)
4596         dispatchMouseEvent(ACTION_DOWN, coords)
4597         dispatchMouseEvent(ACTION_MOVE, coords, Offset(0f, coords.size.height - 1f))
4598         dispatchMouseEvent(ACTION_UP, coords, Offset(0f, coords.size.height - 1f))
4599         dispatchMouseEvent(ACTION_HOVER_ENTER, coords, Offset(0f, coords.size.height - 1f))
4600         rule.runOnUiThread {
4601             assertThat(eventLog).hasSize(5)
4602             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4603             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
4604             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Move)
4605             assertThat(eventLog[3].type).isEqualTo(PointerEventType.Release)
4606             assertThat(eventLog[4].type).isEqualTo(PointerEventType.Exit)
4607         }
4608     }
4609 
4610     private fun setSimpleLayout(eventLog: MutableList<PointerEvent>): LayoutCoordinates {
4611         var innerCoordinates: LayoutCoordinates? = null
4612         val latch = CountDownLatch(1)
4613         rule.runOnUiThread {
4614             container.setContent {
4615                 Box(
4616                     Modifier.fillMaxSize()
4617                         .pointerInput(Unit) {
4618                             awaitPointerEventScope {
4619                                 while (true) {
4620                                     val event = awaitPointerEvent()
4621                                     event.changes[0].consume()
4622                                     eventLog += event
4623                                 }
4624                             }
4625                         }
4626                         .onGloballyPositioned {
4627                             innerCoordinates = it
4628                             latch.countDown()
4629                         }
4630                 )
4631             }
4632         }
4633         assertTrue(latch.await(1, TimeUnit.SECONDS))
4634         return innerCoordinates!!
4635     }
4636 
4637     @Test
4638     fun cancelOnDeviceChange() {
4639         // When a pointer has had a surprise removal, a "cancel" event should be sent if it was
4640         // pressed.
4641         val eventLog = mutableListOf<PointerEvent>()
4642         val coords = setSimpleLayout(eventLog)
4643 
4644         dispatchMouseEvent(ACTION_HOVER_ENTER, coords)
4645         dispatchMouseEvent(ACTION_DOWN, coords)
4646         dispatchMouseEvent(ACTION_MOVE, coords, Offset(0f, 1f))
4647 
4648         val motionEvent =
4649             MotionEvent(
4650                 5,
4651                 ACTION_DOWN,
4652                 1,
4653                 0,
4654                 arrayOf(PointerProperties(10).also { it.toolType = TOOL_TYPE_FINGER }),
4655                 arrayOf(PointerCoords(1f, 1f))
4656             )
4657 
4658         container.dispatchTouchEvent(motionEvent)
4659         rule.runOnUiThread {
4660             assertThat(eventLog).hasSize(5)
4661             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4662             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
4663             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Move)
4664             assertThat(eventLog[3].type).isEqualTo(PointerEventType.Release)
4665             assertThat(eventLog[4].type).isEqualTo(PointerEventType.Press)
4666         }
4667     }
4668 
4669     @Test
4670     fun testSyntheticEventPosition() {
4671         val eventLog = mutableListOf<PointerEvent>()
4672         val coords = setSimpleLayout(eventLog)
4673         dispatchMouseEvent(ACTION_DOWN, coords)
4674 
4675         rule.runOnUiThread {
4676             assertThat(eventLog).hasSize(2)
4677             assertThat(eventLog[0].changes[0].position).isEqualTo(eventLog[1].changes[0].position)
4678         }
4679     }
4680 
4681     @Test
4682     fun testStylusHoverExitDueToPress() {
4683         val eventLog = mutableListOf<PointerEvent>()
4684         val coords = setSimpleLayout(eventLog)
4685 
4686         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_ENTER)
4687         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_EXIT, ACTION_DOWN)
4688 
4689         rule.runOnUiThread {
4690             assertThat(eventLog).hasSize(2)
4691             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4692             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
4693         }
4694     }
4695 
4696     @Test
4697     fun testStylusHoverExitNoFollowingEvent() {
4698         val eventLog = mutableListOf<PointerEvent>()
4699         val coords = setSimpleLayout(eventLog)
4700 
4701         // Exit followed by nothing should just send the exit
4702         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_ENTER)
4703         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_EXIT)
4704 
4705         // Hover exit events in Compose are always delayed two frames to ensure Compose does not
4706         // trigger them if they are followed by a press in the next frame. This accounts for that.
4707         rule.waitForFutureFrame(2)
4708 
4709         rule.runOnUiThread {
4710             assertThat(eventLog).hasSize(2)
4711             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4712             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Exit)
4713         }
4714     }
4715 
4716     @Test
4717     fun testStylusHoverExitWithFollowingHoverEvent() {
4718         val eventLog = mutableListOf<PointerEvent>()
4719         val coords = setSimpleLayout(eventLog)
4720 
4721         // Exit immediately followed by enter with the same device should send both
4722         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_ENTER)
4723         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_EXIT, ACTION_HOVER_ENTER)
4724 
4725         rule.runOnUiThread {
4726             assertThat(eventLog).hasSize(3)
4727             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4728             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Exit)
4729             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Enter)
4730         }
4731     }
4732 
4733     @Test
4734     fun testStylusHoverExitWithFollowingTouchEvent() {
4735         val eventLog = mutableListOf<PointerEvent>()
4736         val coords = setSimpleLayout(eventLog)
4737 
4738         // Exit followed by cancel should send both
4739         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_ENTER)
4740         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_EXIT, ACTION_CANCEL)
4741 
4742         rule.runOnUiThread {
4743             assertThat(eventLog).hasSize(2)
4744             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4745             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Exit)
4746         }
4747     }
4748 
4749     @Test
4750     fun testStylusHoverExitWithFollowingDownOnDifferentDevice() {
4751         val eventLog = mutableListOf<PointerEvent>()
4752         val coords = setSimpleLayout(eventLog)
4753 
4754         // Exit followed by a different device should send the exit
4755         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_ENTER)
4756         rule.runOnUiThread {
4757             val root = coords.findRootCoordinates()
4758             val pos = root.localPositionOf(coords, Offset.Zero)
4759             val androidComposeView = findAndroidComposeView(container) as AndroidComposeView
4760             val exit =
4761                 MotionEvent(
4762                     0,
4763                     ACTION_HOVER_EXIT,
4764                     1,
4765                     0,
4766                     arrayOf(
4767                         PointerProperties(0).also { it.toolType = MotionEvent.TOOL_TYPE_STYLUS }
4768                     ),
4769                     arrayOf(PointerCoords(pos.x, pos.y))
4770                 )
4771 
4772             androidComposeView.dispatchHoverEvent(exit)
4773 
4774             val down =
4775                 MotionEvent(
4776                     0,
4777                     ACTION_DOWN,
4778                     1,
4779                     0,
4780                     arrayOf(PointerProperties(0).also { it.toolType = TOOL_TYPE_FINGER }),
4781                     arrayOf(PointerCoords(pos.x, pos.y))
4782                 )
4783             androidComposeView.dispatchTouchEvent(down)
4784         }
4785 
4786         rule.runOnUiThread {
4787             assertThat(eventLog).hasSize(3)
4788             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4789             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Exit)
4790             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Press)
4791         }
4792     }
4793 
4794     @Test
4795     fun syntheticEventSentAfterUp() {
4796         val eventLog = mutableListOf<PointerEvent>()
4797         val coords = setSimpleLayout(eventLog)
4798 
4799         dispatchMouseEvent(ACTION_HOVER_ENTER, coords)
4800         dispatchMouseEvent(ACTION_DOWN, coords)
4801         dispatchMouseEvent(ACTION_UP, coords)
4802         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_ENTER)
4803 
4804         rule.runOnUiThread {
4805             assertThat(eventLog).hasSize(5)
4806             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
4807             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
4808             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Release)
4809             assertThat(eventLog[3].type).isEqualTo(PointerEventType.Exit)
4810             assertThat(eventLog[4].type).isEqualTo(PointerEventType.Enter)
4811         }
4812     }
4813 
4814     @Test
4815     fun clippedHasNoInputIfLargeEnough() {
4816         val eventLog = mutableListOf<PointerEventType>()
4817         var innerCoordinates: LayoutCoordinates? = null
4818         val latch = CountDownLatch(1)
4819         rule.runOnUiThread {
4820             container.setContent {
4821                 Column(Modifier.fillMaxSize()) {
4822                     Box(
4823                         Modifier.size(50.dp).pointerInput(Unit) {
4824                             awaitPointerEventScope {
4825                                 while (true) {
4826                                     awaitPointerEvent()
4827                                 }
4828                             }
4829                         }
4830                     )
4831                     Box(Modifier.size(50.dp).clipToBounds()) {
4832                         Box(
4833                             Modifier.size(50.dp)
4834                                 .graphicsLayer { translationY = -25.dp.roundToPx().toFloat() }
4835                                 .pointerInput(Unit) {
4836                                     awaitPointerEventScope {
4837                                         while (true) {
4838                                             val event = awaitPointerEvent()
4839                                             event.changes[0].consume()
4840                                             eventLog += event.type
4841                                         }
4842                                     }
4843                                 }
4844                                 .onGloballyPositioned {
4845                                     innerCoordinates = it
4846                                     latch.countDown()
4847                                 }
4848                         )
4849                     }
4850                 }
4851             }
4852         }
4853         assertTrue(latch.await(1, TimeUnit.SECONDS))
4854 
4855         val coords = innerCoordinates!!
4856 
4857         // Hit the top Box
4858         dispatchMouseEvent(ACTION_HOVER_ENTER, coords, Offset(0f, -1f))
4859 
4860         // Hit the bottom box, but clipped
4861         dispatchMouseEvent(ACTION_HOVER_MOVE, coords)
4862         dispatchMouseEvent(
4863             ACTION_HOVER_MOVE,
4864             coords,
4865             Offset(0f, (coords.size.height / 2 - 1).toFloat())
4866         )
4867 
4868         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
4869 
4870         // Now hit the box in the unclipped region
4871         dispatchMouseEvent(
4872             ACTION_HOVER_MOVE,
4873             coords,
4874             Offset(0f, (coords.size.height / 2 + 1).toFloat())
4875         )
4876 
4877         // Now hit the bottom of the clipped region
4878         dispatchMouseEvent(
4879             ACTION_HOVER_MOVE,
4880             coords,
4881             Offset(0f, (coords.size.height - 1).toFloat())
4882         )
4883 
4884         // Now leave
4885         dispatchMouseEvent(ACTION_HOVER_MOVE, coords, Offset(0f, coords.size.height.toFloat() + 1f))
4886 
4887         rule.runOnUiThread {
4888             assertThat(eventLog)
4889                 .containsExactly(
4890                     PointerEventType.Enter,
4891                     PointerEventType.Move,
4892                     PointerEventType.Exit
4893                 )
4894         }
4895     }
4896 
4897     @Test
4898     fun unclippedTakesPrecedenceWithMinimumTouchTarget() {
4899         val eventLog = mutableListOf<PointerEventType>()
4900         var innerCoordinates: LayoutCoordinates? = null
4901         val latch = CountDownLatch(1)
4902         rule.runOnUiThread {
4903             container.setContent {
4904                 Column(Modifier.fillMaxSize()) {
4905                     Box(
4906                         Modifier.size(50.dp).pointerInput(Unit) {
4907                             awaitPointerEventScope {
4908                                 while (true) {
4909                                     awaitPointerEvent()
4910                                 }
4911                             }
4912                         }
4913                     )
4914                     Box(Modifier.size(20.dp).clipToBounds()) {
4915                         Box(
4916                             Modifier.size(20.dp)
4917                                 .graphicsLayer { translationY = -10.dp.roundToPx().toFloat() }
4918                                 .pointerInput(Unit) {
4919                                     awaitPointerEventScope {
4920                                         while (true) {
4921                                             val event = awaitPointerEvent()
4922                                             event.changes[0].consume()
4923                                             eventLog += event.type
4924                                         }
4925                                     }
4926                                 }
4927                                 .onGloballyPositioned {
4928                                     innerCoordinates = it
4929                                     latch.countDown()
4930                                 }
4931                         )
4932                     }
4933                 }
4934             }
4935         }
4936         assertTrue(latch.await(1, TimeUnit.SECONDS))
4937 
4938         val coords = innerCoordinates!!
4939 
4940         // Hit the top Box, but in the minimum touch target area of the bottom Box
4941         dispatchTouchEvent(ACTION_DOWN, coords, Offset(0f, -1f))
4942         dispatchTouchEvent(ACTION_UP, coords, Offset(0f, -1f))
4943 
4944         // Hit the top Box in the clipped region of the bottom Box
4945         dispatchMouseEvent(ACTION_DOWN, coords)
4946         dispatchMouseEvent(ACTION_UP, coords)
4947 
4948         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
4949 
4950         // Hit the bottom box in the unclipped region
4951         val topOfUnclipped = Offset(0f, (coords.size.height / 2 + 1).toFloat())
4952         dispatchMouseEvent(ACTION_DOWN, coords, topOfUnclipped)
4953         dispatchMouseEvent(ACTION_UP, coords, topOfUnclipped)
4954 
4955         // Continue to the bottom of the bottom Box
4956         val bottomOfBox = Offset(0f, (coords.size.height - 1).toFloat())
4957         dispatchMouseEvent(ACTION_DOWN, coords, bottomOfBox)
4958         dispatchMouseEvent(ACTION_UP, coords, bottomOfBox)
4959 
4960         // Now exit the bottom box
4961         val justBelow = Offset(0f, (coords.size.height + 1).toFloat())
4962         dispatchMouseEvent(ACTION_DOWN, coords, justBelow)
4963         dispatchMouseEvent(ACTION_UP, coords, justBelow)
4964 
4965         rule.runOnUiThread {
4966             assertThat(eventLog)
4967                 .containsExactly(
4968                     PointerEventType.Press,
4969                     PointerEventType.Release,
4970                     PointerEventType.Press,
4971                     PointerEventType.Release,
4972                     PointerEventType.Press,
4973                     PointerEventType.Release,
4974                 )
4975         }
4976     }
4977 
4978     /**
4979      * Child of a **clipped** parent uses .graphicsLayer { translationY } to offset OUTSIDE the
4980      * parent's clipped dimensions. Any touch input outside the clipped parent (including both
4981      * direct hits and indirect hits in the minimum touch target) should not be triggered in any
4982      * children. This test calculates the edges using the minimum touch target size (48.dp).
4983      */
4984     @Test
4985     fun clippedWithMinimumTouchTargetOverlap_shouldNotTriggerOverlappingClippedTouch() {
4986         val eventLog = mutableListOf<PointerEventType>()
4987         var innerOffsetBoxCoordinates: LayoutCoordinates? = null
4988         var parentBoxCoordinates: LayoutCoordinates? = null
4989         val latch = CountDownLatch(2)
4990 
4991         var dpInPixel: Float? = null
4992 
4993         rule.runOnUiThread {
4994             container.setContent {
4995                 with(LocalDensity.current) { dpInPixel = 1.dp.toPx() }
4996                 Column(Modifier.background(Color.Cyan).fillMaxSize()) {
4997                     // Top Box
4998                     Box(Modifier.background(Color.Gray).size(100.dp))
4999                     // Bottom Box (parent)
5000                     Box(
5001                         Modifier.background(Color.Red)
5002                             .size(40.dp)
5003                             .clipToBounds()
5004                             .onGloballyPositioned {
5005                                 parentBoxCoordinates = it
5006                                 latch.countDown()
5007                             }
5008                     ) {
5009                         // Inner Bottom Box (this clipped child is the main box we are testing)
5010                         Box(
5011                             Modifier.size(40.dp)
5012                                 .background(Color.Green)
5013                                 // Moves the box outside of the clipped area of the parent
5014                                 .graphicsLayer { translationY = -10.dp.roundToPx().toFloat() }
5015                                 .pointerInput(Unit) {
5016                                     awaitPointerEventScope {
5017                                         while (true) {
5018                                             val event = awaitPointerEvent()
5019                                             event.changes[0].consume()
5020                                             eventLog += event.type
5021                                         }
5022                                     }
5023                                 }
5024                                 .onGloballyPositioned {
5025                                     innerOffsetBoxCoordinates = it
5026                                     latch.countDown()
5027                                 }
5028                         )
5029                     }
5030                 }
5031             }
5032         }
5033         assertTrue(latch.await(1, TimeUnit.SECONDS))
5034 
5035         val offsetBoxCoords: LayoutCoordinates = innerOffsetBoxCoordinates!!
5036         val parentBoxCoords: LayoutCoordinates = parentBoxCoordinates!!
5037 
5038         val justOutsideMinimumTouchTargetOfClippedChild = dpInPixel!! * 5
5039         val edgeOfMinimumTouchTargetOfClippedChild = dpInPixel!! * 4
5040 
5041         // Hits the top Box, but just outside the minimum touch target area of the bottom child Box
5042         // (which includes the offset into top box).
5043         dispatchTouchEvent(
5044             ACTION_DOWN,
5045             offsetBoxCoords,
5046             Offset(0f, -justOutsideMinimumTouchTargetOfClippedChild)
5047         )
5048         dispatchTouchEvent(
5049             ACTION_UP,
5050             offsetBoxCoords,
5051             Offset(0f, -justOutsideMinimumTouchTargetOfClippedChild)
5052         )
5053         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5054 
5055         // Hits the top Box and the minimum touch target area edge of the bottom child Box (which
5056         // includes the offset into top box). Despite this being in the minimum touch area, it will
5057         // NOT trigger the event since it's in the clipped region.
5058         dispatchTouchEvent(
5059             ACTION_DOWN,
5060             offsetBoxCoords,
5061             Offset(0f, -edgeOfMinimumTouchTargetOfClippedChild)
5062         )
5063         dispatchTouchEvent(
5064             ACTION_UP,
5065             offsetBoxCoords,
5066             Offset(0f, -edgeOfMinimumTouchTargetOfClippedChild)
5067         )
5068 
5069         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5070 
5071         // Hits the top Box and a direct hit of the edge of the bottom child Box (which
5072         // includes the offset into top box). It will NOT trigger the event since it's in the
5073         // clipped region.
5074         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords)
5075         dispatchMouseEvent(ACTION_UP, offsetBoxCoords)
5076 
5077         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5078 
5079         // Hits the top Box and a direct hit of the bottom child Box (which
5080         // includes the offset into top box). It's one dp shy of the edge of the parent bottom box.
5081         // It will NOT trigger the event since it's still in the clipped region.
5082         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords, Offset(0f, -dpInPixel!!))
5083         dispatchMouseEvent(ACTION_UP, parentBoxCoords, Offset(0f, -dpInPixel!!))
5084 
5085         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5086 
5087         // Direct hit of edge of bottom parent box (and child box) in an unclipped region.
5088         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords)
5089         dispatchMouseEvent(ACTION_UP, parentBoxCoords)
5090 
5091         rule.runOnUiThread {
5092             assertThat(eventLog)
5093                 .containsExactly(
5094                     PointerEventType.Press,
5095                     PointerEventType.Release,
5096                 )
5097         }
5098 
5099         // Hits the bottom box in the unclipped region (farther down from edges).
5100         val topOfUnclipped = Offset(0f, (offsetBoxCoords.size.height / 2 + 1).toFloat())
5101         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, topOfUnclipped)
5102         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, topOfUnclipped)
5103 
5104         // Continue to the bottom of the bottom Box
5105         val bottomOfBox = Offset(0f, (offsetBoxCoords.size.height - 1).toFloat())
5106         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, bottomOfBox)
5107         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, bottomOfBox)
5108 
5109         // Now exit the bottom box
5110         val justBelow = Offset(0f, (offsetBoxCoords.size.height + 1).toFloat())
5111         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, justBelow)
5112         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, justBelow)
5113 
5114         rule.runOnUiThread {
5115             assertThat(eventLog)
5116                 .containsExactly(
5117                     PointerEventType.Press,
5118                     PointerEventType.Release,
5119                     PointerEventType.Press,
5120                     PointerEventType.Release,
5121                     PointerEventType.Press,
5122                     PointerEventType.Release,
5123                     PointerEventType.Press,
5124                     PointerEventType.Release,
5125                 )
5126         }
5127     }
5128 
5129     /**
5130      * Same as clippedWithMinimumTouchTargetOverlap_shouldNotTriggerOverlappingClippedTouch() but
5131      * uses .offset() instead of .graphicsLayer { translationY }.
5132      */
5133     @Test
5134     fun clippedWithMinimumTouchTargetOverlapViaOffset_shouldNotTriggerOverlappingClippedTouch() {
5135         val eventLog = mutableListOf<PointerEventType>()
5136         var innerOffsetBoxCoordinates: LayoutCoordinates? = null
5137         var parentBoxCoordinates: LayoutCoordinates? = null
5138         val latch = CountDownLatch(2)
5139 
5140         var dpInPixel: Float? = null
5141 
5142         rule.runOnUiThread {
5143             container.setContent {
5144                 with(LocalDensity.current) { dpInPixel = 1.dp.toPx() }
5145                 Column(Modifier.background(Color.Cyan).fillMaxSize()) {
5146                     // Top Box
5147                     Box(Modifier.background(Color.Gray).size(100.dp))
5148                     // Bottom Box (parent)
5149                     Box(
5150                         Modifier.background(Color.Red)
5151                             .size(40.dp)
5152                             .clipToBounds()
5153                             .onGloballyPositioned {
5154                                 parentBoxCoordinates = it
5155                                 latch.countDown()
5156                             }
5157                     ) {
5158                         // Inner Bottom Box (this clipped child is the main box we are testing)
5159                         Box(
5160                             Modifier.size(40.dp)
5161                                 .background(Color.Green)
5162                                 // Moves the box outside of the clipped area of the parent
5163                                 .offset(x = 0.dp, y = (-10).dp)
5164                                 .pointerInput(Unit) {
5165                                     awaitPointerEventScope {
5166                                         while (true) {
5167                                             val event = awaitPointerEvent()
5168                                             event.changes[0].consume()
5169                                             eventLog += event.type
5170                                         }
5171                                     }
5172                                 }
5173                                 .onGloballyPositioned {
5174                                     innerOffsetBoxCoordinates = it
5175                                     latch.countDown()
5176                                 }
5177                         )
5178                     }
5179                 }
5180             }
5181         }
5182         assertTrue(latch.await(1, TimeUnit.SECONDS))
5183 
5184         val offsetBoxCoords: LayoutCoordinates = innerOffsetBoxCoordinates!!
5185         val parentBoxCoords: LayoutCoordinates = parentBoxCoordinates!!
5186 
5187         val justOutsideMinimumTouchTargetOfClippedChild = dpInPixel!! * 5
5188         val edgeOfMinimumTouchTargetOfClippedChild = dpInPixel!! * 4
5189 
5190         // Hits the top Box, but just outside the minimum touch target area of the bottom child Box
5191         // (which includes the offset into top box).
5192         dispatchTouchEvent(
5193             ACTION_DOWN,
5194             offsetBoxCoords,
5195             Offset(0f, -justOutsideMinimumTouchTargetOfClippedChild)
5196         )
5197         dispatchTouchEvent(
5198             ACTION_UP,
5199             offsetBoxCoords,
5200             Offset(0f, -justOutsideMinimumTouchTargetOfClippedChild)
5201         )
5202         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5203 
5204         // Hit the top Box, but in the minimum touch target area of the bottom Box (that hit area
5205         // is WITHIN the clipped region but minimum area hit trumps clip and it will be triggered).
5206         // Note: This is not a direct hit of the bottom box.
5207 
5208         // Hits the top Box and the minimum touch target area edge of the bottom child Box (which
5209         // includes the offset into top box). Despite this being in the minimum touch area, it will
5210         // NOT trigger the event since it's in the clipped region.
5211         dispatchTouchEvent(
5212             ACTION_DOWN,
5213             offsetBoxCoords,
5214             Offset(0f, -edgeOfMinimumTouchTargetOfClippedChild)
5215         )
5216         dispatchTouchEvent(
5217             ACTION_UP,
5218             offsetBoxCoords,
5219             Offset(0f, -edgeOfMinimumTouchTargetOfClippedChild)
5220         )
5221         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5222 
5223         // Hits the top Box and a direct hit of the edge of the bottom child Box (which
5224         // includes the offset into top box). It will NOT trigger the event since it's in the
5225         // clipped region.
5226         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords)
5227         dispatchMouseEvent(ACTION_UP, offsetBoxCoords)
5228 
5229         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5230 
5231         // Hits the top Box and a direct hit of the bottom child Box (which
5232         // includes the offset into top box). It's one dp shy of the edge of the parent bottom box.
5233         // It will NOT trigger the event since it's still in the clipped region.
5234         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords, Offset(0f, -dpInPixel!!))
5235         dispatchMouseEvent(ACTION_UP, parentBoxCoords, Offset(0f, -dpInPixel!!))
5236 
5237         rule.runOnUiThread { assertThat(eventLog).isEmpty() }
5238 
5239         // Direct hit of edge of bottom parent box (and child box) in an unclipped region.
5240         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords)
5241         dispatchMouseEvent(ACTION_UP, parentBoxCoords)
5242 
5243         rule.runOnUiThread {
5244             assertThat(eventLog)
5245                 .containsExactly(
5246                     PointerEventType.Press,
5247                     PointerEventType.Release,
5248                 )
5249         }
5250 
5251         // Hits the bottom box in the unclipped region (farther down from edges).
5252         val topOfUnclipped = Offset(0f, (offsetBoxCoords.size.height / 2 + 1).toFloat())
5253         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, topOfUnclipped)
5254         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, topOfUnclipped)
5255 
5256         // Continue to the bottom of the bottom Box
5257         val bottomOfBox = Offset(0f, (offsetBoxCoords.size.height - 1).toFloat())
5258         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, bottomOfBox)
5259         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, bottomOfBox)
5260 
5261         // Now exit the bottom box
5262         val justBelow = Offset(0f, (offsetBoxCoords.size.height + 1).toFloat())
5263         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, justBelow)
5264         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, justBelow)
5265 
5266         rule.runOnUiThread {
5267             assertThat(eventLog)
5268                 .containsExactly(
5269                     PointerEventType.Press,
5270                     PointerEventType.Release,
5271                     PointerEventType.Press,
5272                     PointerEventType.Release,
5273                     PointerEventType.Press,
5274                     PointerEventType.Release,
5275                     PointerEventType.Press,
5276                     PointerEventType.Release,
5277                 )
5278         }
5279     }
5280 
5281     /**
5282      * Child of a parent uses .graphicsLayer { translationY } to offset OUTSIDE the parent's
5283      * dimensions. Any touch input outside the parent must be a direct hit to win (not indirect in
5284      * minimum test target size). This test calculates the edges using the minimum touch target size
5285      * (48.dp). Also, tests pruning a one node tree in HitTestResult.
5286      *
5287      * TODO(jjw): Write test for this in lower level test file.
5288      */
5289     @Test
5290     fun minimumTouchTargetOverlap_triggersDirectHitWithHigherOrder() {
5291         val eventLogTopBox = mutableListOf<PointerEventType>()
5292         val eventLogBottomBox = mutableListOf<PointerEventType>()
5293         var innerOffsetBoxCoordinates: LayoutCoordinates? = null
5294         var parentBoxCoordinates: LayoutCoordinates? = null
5295         val latch = CountDownLatch(2)
5296 
5297         var dpInPixel: Float? = null
5298 
5299         rule.runOnUiThread {
5300             container.setContent {
5301                 with(LocalDensity.current) { dpInPixel = 1.dp.toPx() }
5302                 Column(Modifier.background(Color.Cyan).fillMaxSize()) {
5303                     // Top Box
5304                     Box(
5305                         Modifier.background(Color.Gray).size(100.dp).pointerInput(Unit) {
5306                             awaitPointerEventScope {
5307                                 while (true) {
5308                                     val event = awaitPointerEvent()
5309                                     event.changes[0].consume()
5310                                     eventLogTopBox += event.type
5311                                 }
5312                             }
5313                         }
5314                     )
5315                     // Bottom Box (parent)
5316                     Box(
5317                         Modifier.background(Color.Red).size(40.dp).onGloballyPositioned {
5318                             parentBoxCoordinates = it
5319                             latch.countDown()
5320                         }
5321                     ) {
5322                         // Inner Bottom Box (main box we are testing)
5323                         Box(
5324                             Modifier.size(40.dp)
5325                                 .background(Color.Green)
5326                                 // Moves the box outside of the parent
5327                                 .graphicsLayer { translationY = -10.dp.roundToPx().toFloat() }
5328                                 .pointerInput(Unit) {
5329                                     awaitPointerEventScope {
5330                                         while (true) {
5331                                             val event = awaitPointerEvent()
5332                                             event.changes[0].consume()
5333                                             eventLogBottomBox += event.type
5334                                         }
5335                                     }
5336                                 }
5337                                 .onGloballyPositioned {
5338                                     innerOffsetBoxCoordinates = it
5339                                     latch.countDown()
5340                                 }
5341                         )
5342                     }
5343                 }
5344             }
5345         }
5346         assertTrue(latch.await(1, TimeUnit.SECONDS))
5347 
5348         val offsetBoxCoords: LayoutCoordinates = innerOffsetBoxCoordinates!!
5349         val parentBoxCoords: LayoutCoordinates = parentBoxCoordinates!!
5350 
5351         val justOutsideMinimumTouchTargetOfChildBox = dpInPixel!! * 5
5352         val edgeOfMinimumTouchTargetOfChildBox = dpInPixel!! * 4
5353 
5354         // Hits the top Box, but just outside the minimum touch target area of the bottom child Box
5355         // (which includes the offset into top box).
5356         dispatchTouchEvent(
5357             ACTION_DOWN,
5358             offsetBoxCoords,
5359             Offset(0f, -justOutsideMinimumTouchTargetOfChildBox)
5360         )
5361         dispatchTouchEvent(
5362             ACTION_UP,
5363             offsetBoxCoords,
5364             Offset(0f, -justOutsideMinimumTouchTargetOfChildBox)
5365         )
5366 
5367         rule.runOnUiThread {
5368             assertThat(eventLogTopBox)
5369                 .containsExactly(
5370                     PointerEventType.Press,
5371                     PointerEventType.Release,
5372                 )
5373             assertThat(eventLogBottomBox).isEmpty()
5374         }
5375 
5376         // Hit the top Box, but in the minimum touch target area of the bottom Box. Because this is
5377         // not a direct hit on the bottom box, the top box still wins.
5378         dispatchTouchEvent(
5379             ACTION_DOWN,
5380             offsetBoxCoords,
5381             Offset(0f, -edgeOfMinimumTouchTargetOfChildBox)
5382         )
5383         dispatchTouchEvent(
5384             ACTION_UP,
5385             offsetBoxCoords,
5386             Offset(0f, -edgeOfMinimumTouchTargetOfChildBox)
5387         )
5388 
5389         rule.runOnUiThread {
5390             assertThat(eventLogTopBox)
5391                 .containsExactly(
5392                     PointerEventType.Press,
5393                     PointerEventType.Release,
5394                     PointerEventType.Press,
5395                     PointerEventType.Release,
5396                 )
5397             assertThat(eventLogBottomBox).isEmpty()
5398         }
5399 
5400         // Hits the top Box and a direct hit of the edge of the bottom child Box (which
5401         // includes the offset into top box). Bottom child will get the event since it has higher
5402         // order of the two direct hits.
5403         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords)
5404         dispatchMouseEvent(ACTION_UP, offsetBoxCoords)
5405 
5406         rule.runOnUiThread {
5407             assertThat(eventLogTopBox)
5408                 .containsExactly(
5409                     PointerEventType.Press,
5410                     PointerEventType.Release,
5411                     PointerEventType.Press,
5412                     PointerEventType.Release,
5413                 )
5414             assertThat(eventLogBottomBox)
5415                 .containsExactly(
5416                     PointerEventType.Enter,
5417                     PointerEventType.Press,
5418                     PointerEventType.Release,
5419                 )
5420         }
5421 
5422         // Still a direct hit on bottom child box, so it wins.
5423         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords, Offset(0f, -dpInPixel!!))
5424         dispatchMouseEvent(ACTION_UP, parentBoxCoords, Offset(0f, -dpInPixel!!))
5425 
5426         rule.runOnUiThread {
5427             assertThat(eventLogTopBox)
5428                 .containsExactly(
5429                     PointerEventType.Press,
5430                     PointerEventType.Release,
5431                     PointerEventType.Press,
5432                     PointerEventType.Release,
5433                 )
5434             assertThat(eventLogBottomBox)
5435                 .containsExactly(
5436                     PointerEventType.Enter,
5437                     PointerEventType.Press,
5438                     PointerEventType.Release,
5439                     PointerEventType.Press,
5440                     PointerEventType.Release,
5441                 )
5442         }
5443 
5444         // Direct hit of edge of bottom parent box (and child box).
5445         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords)
5446         dispatchMouseEvent(ACTION_UP, parentBoxCoords)
5447 
5448         rule.runOnUiThread {
5449             assertThat(eventLogTopBox)
5450                 .containsExactly(
5451                     PointerEventType.Press,
5452                     PointerEventType.Release,
5453                     PointerEventType.Press,
5454                     PointerEventType.Release,
5455                 )
5456             assertThat(eventLogBottomBox)
5457                 .containsExactly(
5458                     PointerEventType.Enter,
5459                     PointerEventType.Press,
5460                     PointerEventType.Release,
5461                     PointerEventType.Press,
5462                     PointerEventType.Release,
5463                     PointerEventType.Press,
5464                     PointerEventType.Release,
5465                 )
5466         }
5467 
5468         // Hits the bottom box (farther down from edges).
5469         val topOfUnclipped = Offset(0f, (offsetBoxCoords.size.height / 2 + 1).toFloat())
5470         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, topOfUnclipped)
5471         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, topOfUnclipped)
5472 
5473         // Continue to the bottom of the bottom Box
5474         val bottomOfBox = Offset(0f, (offsetBoxCoords.size.height - 1).toFloat())
5475         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, bottomOfBox)
5476         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, bottomOfBox)
5477 
5478         // Now exit the bottom box
5479         val justBelow = Offset(0f, (offsetBoxCoords.size.height + 1).toFloat())
5480         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords, justBelow)
5481         dispatchMouseEvent(ACTION_UP, offsetBoxCoords, justBelow)
5482 
5483         rule.runOnUiThread {
5484             assertThat(eventLogTopBox)
5485                 .containsExactly(
5486                     PointerEventType.Press,
5487                     PointerEventType.Release,
5488                     PointerEventType.Press,
5489                     PointerEventType.Release,
5490                 )
5491             assertThat(eventLogBottomBox)
5492                 .containsExactly(
5493                     PointerEventType.Enter,
5494                     PointerEventType.Press,
5495                     PointerEventType.Release,
5496                     PointerEventType.Press,
5497                     PointerEventType.Release,
5498                     PointerEventType.Press,
5499                     PointerEventType.Release,
5500                     PointerEventType.Press,
5501                     PointerEventType.Release,
5502                     PointerEventType.Press,
5503                     PointerEventType.Release,
5504                     PointerEventType.Press,
5505                     PointerEventType.Release,
5506                 )
5507         }
5508     }
5509 
5510     /**
5511      * Tests proper pointer input results on three nested boxes with different dimensions:
5512      * - Large box 5x the size of the small box
5513      *     - Medium box 3x size of small box (centered in large box as child)
5514      *         - Small box larger than minimum touch target size (centered in medium box as child)
5515      *
5516      * In this test, [Modifier.offset()] is used to center the boxes! The test is meant to test
5517      * modifiers that change the LayoutNode's location while also being large enough to not hit any
5518      * logic associated with the minimum touch target size.
5519      *
5520      * Input hits top left area of large box (triggers large box), the top left area of the medium
5521      * box (triggers large and medium), and the center of the small box (triggers all three boxes).
5522      *
5523      * This test is a high-level implementation of the tests using
5524      * [PointerInputEventProcessorTest.process_partialTreeHits].
5525      */
5526     @Test
5527     fun inputOnNestedBoxesLargerThanMinTouchPlacedViaOffset_simpleInput_properlyTriggers() {
5528         val eventLogLargeBox = mutableListOf<PointerEventType>()
5529         val eventLogMediumBox = mutableListOf<PointerEventType>()
5530         val eventLogSmallBox = mutableListOf<PointerEventType>()
5531 
5532         var layoutCoordsLargeBox: LayoutCoordinates? = null
5533 
5534         // Changes dynamically to size specified by minimumTouchTargetSize.
5535         var minimumTouchTargetSizeDp: Dp
5536 
5537         var dimensionsLargeBoxDp: Dp
5538         var dimensionsMediumBoxDp: Dp
5539         var dimensionsSmallBoxDp: Dp
5540 
5541         var offsetAmountDp: Dp
5542 
5543         var hitAllThreeBoxesFloat: Float? = null
5544         var hitLargeAndMediumBoxesFloat: Float? = null
5545         var hitLargeBoxOnlyFloat: Float? = null
5546 
5547         val latch = CountDownLatch(1)
5548 
5549         rule.runOnUiThread {
5550             container.setContent {
5551                 with(LocalViewConfiguration.current) {
5552                     minimumTouchTargetSizeDp = this.minimumTouchTargetSize.width
5553                 }
5554 
5555                 with(LocalDensity.current) {
5556                     val baseSize = roundUpDpToNearestHundred(minimumTouchTargetSizeDp)
5557                     dimensionsSmallBoxDp = baseSize
5558                     dimensionsMediumBoxDp = baseSize * 3
5559                     dimensionsLargeBoxDp = baseSize * 5
5560 
5561                     // Just happens to be the same dimensions as the bottom box
5562                     offsetAmountDp = dimensionsSmallBoxDp
5563 
5564                     hitAllThreeBoxesFloat = dimensionsLargeBoxDp.toPx() / 2
5565                     hitLargeAndMediumBoxesFloat = dimensionsMediumBoxDp.toPx() / 2
5566                     hitLargeBoxOnlyFloat = dimensionsSmallBoxDp.toPx() / 2
5567                 }
5568 
5569                 // Large Box (5x the size of the small box)
5570                 Box(
5571                     Modifier.background(Color.Cyan)
5572                         .size(dimensionsLargeBoxDp)
5573                         .pointerInput(Unit) {
5574                             awaitPointerEventScope {
5575                                 while (true) {
5576                                     val event = awaitPointerEvent()
5577                                     event.changes[0].consume()
5578                                     eventLogLargeBox += event.type
5579                                 }
5580                             }
5581                         }
5582                         .onGloballyPositioned {
5583                             layoutCoordsLargeBox = it
5584                             latch.countDown()
5585                         }
5586                 ) {
5587                     // Medium Box (3x the size of the small box)
5588                     Box(
5589                         Modifier.offset(offsetAmountDp, offsetAmountDp)
5590                             .background(Color.Gray)
5591                             .size(dimensionsMediumBoxDp)
5592                             .pointerInput(Unit) {
5593                                 awaitPointerEventScope {
5594                                     while (true) {
5595                                         val event = awaitPointerEvent()
5596                                         event.changes[0].consume()
5597                                         eventLogMediumBox += event.type
5598                                     }
5599                                 }
5600                             }
5601                     ) {
5602                         // Small Box
5603                         Box(
5604                             Modifier.offset(offsetAmountDp, offsetAmountDp)
5605                                 .background(Color.Red)
5606                                 .size(dimensionsSmallBoxDp)
5607                                 .pointerInput(Unit) {
5608                                     awaitPointerEventScope {
5609                                         while (true) {
5610                                             val event = awaitPointerEvent()
5611                                             event.changes[0].consume()
5612                                             eventLogSmallBox += event.type
5613                                         }
5614                                     }
5615                                 }
5616                         )
5617                     }
5618                 }
5619             }
5620         }
5621         assertTrue(latch.await(1, TimeUnit.SECONDS))
5622 
5623         val topOffsetBoxCoords: LayoutCoordinates = layoutCoordsLargeBox!!
5624 
5625         // Hits the large box only (outside of the medium and small boxes [child, grandchild]).
5626         dispatchTouchEvent(
5627             ACTION_DOWN,
5628             topOffsetBoxCoords,
5629             Offset(hitLargeBoxOnlyFloat!!, hitLargeBoxOnlyFloat!!)
5630         )
5631         dispatchTouchEvent(
5632             ACTION_UP,
5633             topOffsetBoxCoords,
5634             Offset(hitLargeBoxOnlyFloat!!, hitLargeBoxOnlyFloat!!)
5635         )
5636 
5637         rule.runOnUiThread {
5638             assertThat(eventLogLargeBox)
5639                 .containsExactly(
5640                     PointerEventType.Press,
5641                     PointerEventType.Release,
5642                 )
5643             assertThat(eventLogMediumBox).isEmpty()
5644             assertThat(eventLogSmallBox).isEmpty()
5645         }
5646 
5647         // Hits the medium boxes inside large box (but of small box [child]).
5648         dispatchTouchEvent(
5649             ACTION_DOWN,
5650             topOffsetBoxCoords,
5651             Offset(hitLargeAndMediumBoxesFloat!!, hitLargeAndMediumBoxesFloat!!)
5652         )
5653         dispatchTouchEvent(
5654             ACTION_UP,
5655             topOffsetBoxCoords,
5656             Offset(hitLargeAndMediumBoxesFloat!!, hitLargeAndMediumBoxesFloat!!)
5657         )
5658 
5659         rule.runOnUiThread {
5660             assertThat(eventLogLargeBox)
5661                 .containsExactly(
5662                     PointerEventType.Press,
5663                     PointerEventType.Release,
5664                     PointerEventType.Press,
5665                     PointerEventType.Release,
5666                 )
5667             assertThat(eventLogMediumBox)
5668                 .containsExactly(
5669                     PointerEventType.Press,
5670                     PointerEventType.Release,
5671                 )
5672             assertThat(eventLogSmallBox).isEmpty()
5673         }
5674 
5675         // Hits the small boxes inside medium box inside large box (that is, hits all).
5676         dispatchTouchEvent(
5677             ACTION_DOWN,
5678             topOffsetBoxCoords,
5679             Offset(hitAllThreeBoxesFloat!!, hitAllThreeBoxesFloat!!)
5680         )
5681         dispatchTouchEvent(
5682             ACTION_UP,
5683             topOffsetBoxCoords,
5684             Offset(hitAllThreeBoxesFloat!!, hitAllThreeBoxesFloat!!)
5685         )
5686 
5687         rule.runOnUiThread {
5688             assertThat(eventLogLargeBox)
5689                 .containsExactly(
5690                     PointerEventType.Press,
5691                     PointerEventType.Release,
5692                     PointerEventType.Press,
5693                     PointerEventType.Release,
5694                     PointerEventType.Press,
5695                     PointerEventType.Release,
5696                 )
5697             assertThat(eventLogMediumBox)
5698                 .containsExactly(
5699                     PointerEventType.Press,
5700                     PointerEventType.Release,
5701                     PointerEventType.Press,
5702                     PointerEventType.Release,
5703                 )
5704             assertThat(eventLogSmallBox)
5705                 .containsExactly(
5706                     PointerEventType.Press,
5707                     PointerEventType.Release,
5708                 )
5709         }
5710     }
5711 
5712     /**
5713      * Same test as
5714      * [inputOnNestedBoxesLargerThanMinTouchPlacedViaOffset_simpleInput_properlyTriggers()] but uses
5715      * [Modifier.graphicsLayer()] instead of [Modifier.offset()] to move the LayoutNodes.
5716      */
5717     @Test
5718     fun inputOnNestedBoxesLargerThanMinTouchPlacedViaGraphicsLayer_simpleInput_properlyTriggers() {
5719         val eventLogLargeBox = mutableListOf<PointerEventType>()
5720         val eventLogMediumBox = mutableListOf<PointerEventType>()
5721         val eventLogSmallBox = mutableListOf<PointerEventType>()
5722 
5723         var layoutCoordsLargeBox: LayoutCoordinates? = null
5724 
5725         // Changes dynamically to size specified by minimumTouchTargetSize.
5726         var minimumTouchTargetSizeDp: Dp
5727 
5728         var dimensionsLargeBoxDp: Dp
5729         var dimensionsMediumBoxDp: Dp
5730         var dimensionsSmallBoxDp: Dp
5731 
5732         var offsetAmountDp: Dp
5733 
5734         var hitAllThreeBoxesFloat: Float? = null
5735         var hitLargeAndMediumBoxesFloat: Float? = null
5736         var hitLargeBoxOnlyFloat: Float? = null
5737 
5738         val latch = CountDownLatch(1)
5739 
5740         rule.runOnUiThread {
5741             container.setContent {
5742                 with(LocalViewConfiguration.current) {
5743                     minimumTouchTargetSizeDp = this.minimumTouchTargetSize.width
5744                 }
5745 
5746                 with(LocalDensity.current) {
5747                     val baseSize = roundUpDpToNearestHundred(minimumTouchTargetSizeDp)
5748                     dimensionsSmallBoxDp = baseSize
5749                     dimensionsMediumBoxDp = baseSize * 3
5750                     dimensionsLargeBoxDp = baseSize * 5
5751 
5752                     // Just happens to be the same dimensions as the bottom box
5753                     offsetAmountDp = dimensionsSmallBoxDp
5754 
5755                     hitAllThreeBoxesFloat = dimensionsLargeBoxDp.toPx() / 2
5756                     hitLargeAndMediumBoxesFloat = dimensionsMediumBoxDp.toPx() / 2
5757                     hitLargeBoxOnlyFloat = dimensionsSmallBoxDp.toPx() / 2
5758                 }
5759 
5760                 // Large Box (5x the size of the small box)
5761                 Box(
5762                     Modifier.background(Color.Cyan)
5763                         .size(dimensionsLargeBoxDp)
5764                         .pointerInput(Unit) {
5765                             awaitPointerEventScope {
5766                                 while (true) {
5767                                     val event = awaitPointerEvent()
5768                                     event.changes[0].consume()
5769                                     eventLogLargeBox += event.type
5770                                 }
5771                             }
5772                         }
5773                         .onGloballyPositioned {
5774                             layoutCoordsLargeBox = it
5775                             latch.countDown()
5776                         }
5777                 ) {
5778                     // Medium Box (3x the size of the small box)
5779                     Box(
5780                         Modifier.graphicsLayer {
5781                                 translationY = offsetAmountDp.toPx()
5782                                 translationX = offsetAmountDp.toPx()
5783                             }
5784                             .background(Color.Gray)
5785                             .size(dimensionsMediumBoxDp)
5786                             .pointerInput(Unit) {
5787                                 awaitPointerEventScope {
5788                                     while (true) {
5789                                         val event = awaitPointerEvent()
5790                                         event.changes[0].consume()
5791                                         eventLogMediumBox += event.type
5792                                     }
5793                                 }
5794                             }
5795                     ) {
5796                         // Small Box
5797                         Box(
5798                             Modifier.graphicsLayer {
5799                                     translationY = offsetAmountDp.toPx()
5800                                     translationX = offsetAmountDp.toPx()
5801                                 }
5802                                 .background(Color.Red)
5803                                 .size(dimensionsSmallBoxDp)
5804                                 .pointerInput(Unit) {
5805                                     awaitPointerEventScope {
5806                                         while (true) {
5807                                             val event = awaitPointerEvent()
5808                                             event.changes[0].consume()
5809                                             eventLogSmallBox += event.type
5810                                         }
5811                                     }
5812                                 }
5813                         )
5814                     }
5815                 }
5816             }
5817         }
5818         assertTrue(latch.await(1, TimeUnit.SECONDS))
5819 
5820         val topOffsetBoxCoords: LayoutCoordinates = layoutCoordsLargeBox!!
5821 
5822         // Hits the large box only (outside of the medium and small boxes [child, grandchild]).
5823         dispatchTouchEvent(
5824             ACTION_DOWN,
5825             topOffsetBoxCoords,
5826             Offset(hitLargeBoxOnlyFloat!!, hitLargeBoxOnlyFloat!!)
5827         )
5828         dispatchTouchEvent(
5829             ACTION_UP,
5830             topOffsetBoxCoords,
5831             Offset(hitLargeBoxOnlyFloat!!, hitLargeBoxOnlyFloat!!)
5832         )
5833 
5834         rule.runOnUiThread {
5835             assertThat(eventLogLargeBox)
5836                 .containsExactly(
5837                     PointerEventType.Press,
5838                     PointerEventType.Release,
5839                 )
5840             assertThat(eventLogMediumBox).isEmpty()
5841             assertThat(eventLogSmallBox).isEmpty()
5842         }
5843 
5844         // Hits the medium boxes inside large box (but of small box [child]).
5845         dispatchTouchEvent(
5846             ACTION_DOWN,
5847             topOffsetBoxCoords,
5848             Offset(hitLargeAndMediumBoxesFloat!!, hitLargeAndMediumBoxesFloat!!)
5849         )
5850         dispatchTouchEvent(
5851             ACTION_UP,
5852             topOffsetBoxCoords,
5853             Offset(hitLargeAndMediumBoxesFloat!!, hitLargeAndMediumBoxesFloat!!)
5854         )
5855 
5856         rule.runOnUiThread {
5857             assertThat(eventLogLargeBox)
5858                 .containsExactly(
5859                     PointerEventType.Press,
5860                     PointerEventType.Release,
5861                     PointerEventType.Press,
5862                     PointerEventType.Release,
5863                 )
5864             assertThat(eventLogMediumBox)
5865                 .containsExactly(
5866                     PointerEventType.Press,
5867                     PointerEventType.Release,
5868                 )
5869             assertThat(eventLogSmallBox).isEmpty()
5870         }
5871 
5872         // Hits the small boxes inside medium box inside large box (that is, hits all).
5873         dispatchTouchEvent(
5874             ACTION_DOWN,
5875             topOffsetBoxCoords,
5876             Offset(hitAllThreeBoxesFloat!!, hitAllThreeBoxesFloat!!)
5877         )
5878         dispatchTouchEvent(
5879             ACTION_UP,
5880             topOffsetBoxCoords,
5881             Offset(hitAllThreeBoxesFloat!!, hitAllThreeBoxesFloat!!)
5882         )
5883 
5884         rule.runOnUiThread {
5885             assertThat(eventLogLargeBox)
5886                 .containsExactly(
5887                     PointerEventType.Press,
5888                     PointerEventType.Release,
5889                     PointerEventType.Press,
5890                     PointerEventType.Release,
5891                     PointerEventType.Press,
5892                     PointerEventType.Release,
5893                 )
5894             assertThat(eventLogMediumBox)
5895                 .containsExactly(
5896                     PointerEventType.Press,
5897                     PointerEventType.Release,
5898                     PointerEventType.Press,
5899                     PointerEventType.Release,
5900                 )
5901             assertThat(eventLogSmallBox)
5902                 .containsExactly(
5903                     PointerEventType.Press,
5904                     PointerEventType.Release,
5905                 )
5906         }
5907     }
5908 
5909     /**
5910      * Same test as
5911      * [inputOnNestedBoxesLargerThanMinTouchPlacedViaOffset_simpleInput_properlyTriggers()] but uses
5912      * [Modifier.padding()] instead of [Modifier.offset()] to move the LayoutNodes.
5913      */
5914     @Test
5915     fun inputOnNestedBoxesLargerThanMinTouchPlacedViaPadding_simpleInput_properlyTriggers() {
5916         val eventLogLargeBox = mutableListOf<PointerEventType>()
5917         val eventLogMediumBox = mutableListOf<PointerEventType>()
5918         val eventLogSmallBox = mutableListOf<PointerEventType>()
5919 
5920         var layoutCoordsLargeBox: LayoutCoordinates? = null
5921 
5922         // Changes dynamically to size specified by minimumTouchTargetSize.
5923         var minimumTouchTargetSizeDp: Dp
5924 
5925         var dimensionsLargeBoxDp: Dp
5926         var dimensionsMediumBoxDp: Dp
5927         var dimensionsSmallBoxDp: Dp
5928 
5929         var offsetAmountDp: Dp
5930 
5931         var hitAllThreeBoxesFloat: Float? = null
5932         var hitLargeAndMediumBoxesFloat: Float? = null
5933         var hitLargeBoxOnlyFloat: Float? = null
5934 
5935         val latch = CountDownLatch(1)
5936 
5937         rule.runOnUiThread {
5938             container.setContent {
5939                 with(LocalViewConfiguration.current) {
5940                     minimumTouchTargetSizeDp = this.minimumTouchTargetSize.width
5941                 }
5942 
5943                 with(LocalDensity.current) {
5944                     val baseSize = roundUpDpToNearestHundred(minimumTouchTargetSizeDp)
5945                     dimensionsSmallBoxDp = baseSize
5946                     dimensionsMediumBoxDp = baseSize * 3
5947                     dimensionsLargeBoxDp = baseSize * 5
5948 
5949                     // Just happens to be the same dimensions as the bottom box
5950                     offsetAmountDp = dimensionsSmallBoxDp
5951 
5952                     hitAllThreeBoxesFloat = dimensionsLargeBoxDp.toPx() / 2
5953                     hitLargeAndMediumBoxesFloat = dimensionsMediumBoxDp.toPx() / 2
5954                     hitLargeBoxOnlyFloat = dimensionsSmallBoxDp.toPx() / 2
5955                 }
5956 
5957                 // Large Box (5x the size of the small box)
5958                 Box(
5959                     Modifier.background(Color.Cyan)
5960                         .size(dimensionsLargeBoxDp)
5961                         .pointerInput(Unit) {
5962                             awaitPointerEventScope {
5963                                 while (true) {
5964                                     val event = awaitPointerEvent()
5965                                     event.changes[0].consume()
5966                                     eventLogLargeBox += event.type
5967                                 }
5968                             }
5969                         }
5970                         .onGloballyPositioned {
5971                             layoutCoordsLargeBox = it
5972                             latch.countDown()
5973                         }
5974                 ) {
5975                     // Medium Box (3x the size of the small box)
5976                     Box(
5977                         Modifier.padding(start = offsetAmountDp, top = offsetAmountDp)
5978                             .background(Color.Gray)
5979                             .size(dimensionsMediumBoxDp)
5980                             .pointerInput(Unit) {
5981                                 awaitPointerEventScope {
5982                                     while (true) {
5983                                         val event = awaitPointerEvent()
5984                                         event.changes[0].consume()
5985                                         eventLogMediumBox += event.type
5986                                     }
5987                                 }
5988                             }
5989                     ) {
5990                         // Small Box
5991                         Box(
5992                             Modifier.padding(start = offsetAmountDp, top = offsetAmountDp)
5993                                 .background(Color.Red)
5994                                 .size(dimensionsSmallBoxDp)
5995                                 .pointerInput(Unit) {
5996                                     awaitPointerEventScope {
5997                                         while (true) {
5998                                             val event = awaitPointerEvent()
5999                                             event.changes[0].consume()
6000                                             eventLogSmallBox += event.type
6001                                         }
6002                                     }
6003                                 }
6004                         )
6005                     }
6006                 }
6007             }
6008         }
6009         assertTrue(latch.await(1, TimeUnit.SECONDS))
6010 
6011         val topOffsetBoxCoords: LayoutCoordinates = layoutCoordsLargeBox!!
6012 
6013         // Hits the large box only (outside of the medium and small boxes [child, grandchild]).
6014         dispatchTouchEvent(
6015             ACTION_DOWN,
6016             topOffsetBoxCoords,
6017             Offset(hitLargeBoxOnlyFloat!!, hitLargeBoxOnlyFloat!!)
6018         )
6019         dispatchTouchEvent(
6020             ACTION_UP,
6021             topOffsetBoxCoords,
6022             Offset(hitLargeBoxOnlyFloat!!, hitLargeBoxOnlyFloat!!)
6023         )
6024 
6025         rule.runOnUiThread {
6026             assertThat(eventLogLargeBox)
6027                 .containsExactly(
6028                     PointerEventType.Press,
6029                     PointerEventType.Release,
6030                 )
6031             assertThat(eventLogMediumBox).isEmpty()
6032             assertThat(eventLogSmallBox).isEmpty()
6033         }
6034 
6035         // Hits the medium boxes inside large box (but of small box [child]).
6036         dispatchTouchEvent(
6037             ACTION_DOWN,
6038             topOffsetBoxCoords,
6039             Offset(hitLargeAndMediumBoxesFloat!!, hitLargeAndMediumBoxesFloat!!)
6040         )
6041         dispatchTouchEvent(
6042             ACTION_UP,
6043             topOffsetBoxCoords,
6044             Offset(hitLargeAndMediumBoxesFloat!!, hitLargeAndMediumBoxesFloat!!)
6045         )
6046 
6047         rule.runOnUiThread {
6048             assertThat(eventLogLargeBox)
6049                 .containsExactly(
6050                     PointerEventType.Press,
6051                     PointerEventType.Release,
6052                     PointerEventType.Press,
6053                     PointerEventType.Release,
6054                 )
6055             assertThat(eventLogMediumBox)
6056                 .containsExactly(
6057                     PointerEventType.Press,
6058                     PointerEventType.Release,
6059                 )
6060             assertThat(eventLogSmallBox).isEmpty()
6061         }
6062 
6063         // Hits the small boxes inside medium box inside large box (that is, hits all).
6064         dispatchTouchEvent(
6065             ACTION_DOWN,
6066             topOffsetBoxCoords,
6067             Offset(hitAllThreeBoxesFloat!!, hitAllThreeBoxesFloat!!)
6068         )
6069         dispatchTouchEvent(
6070             ACTION_UP,
6071             topOffsetBoxCoords,
6072             Offset(hitAllThreeBoxesFloat!!, hitAllThreeBoxesFloat!!)
6073         )
6074 
6075         rule.runOnUiThread {
6076             assertThat(eventLogLargeBox)
6077                 .containsExactly(
6078                     PointerEventType.Press,
6079                     PointerEventType.Release,
6080                     PointerEventType.Press,
6081                     PointerEventType.Release,
6082                     PointerEventType.Press,
6083                     PointerEventType.Release,
6084                 )
6085             assertThat(eventLogMediumBox)
6086                 .containsExactly(
6087                     PointerEventType.Press,
6088                     PointerEventType.Release,
6089                     PointerEventType.Press,
6090                     PointerEventType.Release,
6091                 )
6092             assertThat(eventLogSmallBox)
6093                 .containsExactly(
6094                     PointerEventType.Press,
6095                     PointerEventType.Release,
6096                 )
6097         }
6098     }
6099 
6100     /**
6101      * Same as minimumTouchTargetOverlap_triggersDirectHitWithHigherOrder() but uses .offset()
6102      * instead of .graphicsLayer { translationY }.
6103      */
6104     @Test
6105     fun minimumTouchTargetOverlapWithOffset_triggersDirectHitWithHigherOrder() {
6106         val eventLogTopBox = mutableListOf<PointerEventType>()
6107         val eventLogBottomBox = mutableListOf<PointerEventType>()
6108         var innerOffsetBoxCoordinates: LayoutCoordinates? = null
6109         var parentBoxCoordinates: LayoutCoordinates? = null
6110         val latch = CountDownLatch(2)
6111 
6112         var dpInPixel: Float? = null
6113 
6114         rule.runOnUiThread {
6115             container.setContent {
6116                 with(LocalDensity.current) { dpInPixel = 1.dp.toPx() }
6117                 Column(Modifier.background(Color.Cyan).fillMaxSize()) {
6118                     // Top Box
6119                     Box(
6120                         Modifier.background(Color.Gray).size(100.dp).pointerInput(Unit) {
6121                             awaitPointerEventScope {
6122                                 while (true) {
6123                                     val event = awaitPointerEvent()
6124                                     event.changes[0].consume()
6125                                     eventLogTopBox += event.type
6126                                 }
6127                             }
6128                         }
6129                     )
6130                     // Bottom Box (parent)
6131                     Box(
6132                         Modifier.background(Color.Red).size(40.dp).onGloballyPositioned {
6133                             parentBoxCoordinates = it
6134                             latch.countDown()
6135                         }
6136                     ) {
6137                         // Inner Bottom Box (main box we are testing)
6138                         Box(
6139                             Modifier.size(40.dp)
6140                                 .background(Color.Green)
6141                                 // Moves the box outside of the parent
6142                                 .offset(x = 0.dp, y = (-10).dp)
6143                                 .pointerInput(Unit) {
6144                                     awaitPointerEventScope {
6145                                         while (true) {
6146                                             val event = awaitPointerEvent()
6147                                             event.changes[0].consume()
6148                                             eventLogBottomBox += event.type
6149                                         }
6150                                     }
6151                                 }
6152                                 .onGloballyPositioned {
6153                                     innerOffsetBoxCoordinates = it
6154                                     latch.countDown()
6155                                 }
6156                         )
6157                     }
6158                 }
6159             }
6160         }
6161         assertTrue(latch.await(1, TimeUnit.SECONDS))
6162 
6163         val offsetBoxCoords: LayoutCoordinates = innerOffsetBoxCoordinates!!
6164         val parentBoxCoords: LayoutCoordinates = parentBoxCoordinates!!
6165 
6166         val justOutsideMinimumTouchTargetOfChildBox = dpInPixel!! * 5
6167         val edgeOfMinimumTouchTargetOfChildBox = dpInPixel!! * 4
6168 
6169         // Hits the top Box, but just outside the minimum touch target area of the bottom child Box
6170         // (which includes the offset into top box).
6171         dispatchTouchEvent(
6172             ACTION_DOWN,
6173             offsetBoxCoords,
6174             Offset(0f, -justOutsideMinimumTouchTargetOfChildBox)
6175         )
6176         dispatchTouchEvent(
6177             ACTION_UP,
6178             offsetBoxCoords,
6179             Offset(0f, -justOutsideMinimumTouchTargetOfChildBox)
6180         )
6181 
6182         rule.runOnUiThread {
6183             assertThat(eventLogTopBox)
6184                 .containsExactly(
6185                     PointerEventType.Press,
6186                     PointerEventType.Release,
6187                 )
6188             assertThat(eventLogBottomBox).isEmpty()
6189         }
6190 
6191         // Hit the top Box, but in the minimum touch target area of the bottom Box. Because this is
6192         // not a direct hit on the bottom box, the top box still wins.
6193         dispatchTouchEvent(
6194             ACTION_DOWN,
6195             offsetBoxCoords,
6196             Offset(0f, -edgeOfMinimumTouchTargetOfChildBox)
6197         )
6198         dispatchTouchEvent(
6199             ACTION_UP,
6200             offsetBoxCoords,
6201             Offset(0f, -edgeOfMinimumTouchTargetOfChildBox)
6202         )
6203 
6204         rule.runOnUiThread {
6205             assertThat(eventLogTopBox)
6206                 .containsExactly(
6207                     PointerEventType.Press,
6208                     PointerEventType.Release,
6209                     PointerEventType.Press,
6210                     PointerEventType.Release,
6211                 )
6212             assertThat(eventLogBottomBox).isEmpty()
6213         }
6214 
6215         // Hits the top Box and a direct hit of the edge of the bottom child Box (which
6216         // includes the offset into top box). Bottom child will get the event since it has higher
6217         // order of the two direct hits.
6218         dispatchMouseEvent(ACTION_DOWN, offsetBoxCoords)
6219         dispatchMouseEvent(ACTION_UP, offsetBoxCoords)
6220 
6221         rule.runOnUiThread {
6222             assertThat(eventLogTopBox)
6223                 .containsExactly(
6224                     PointerEventType.Press,
6225                     PointerEventType.Release,
6226                     PointerEventType.Press,
6227                     PointerEventType.Release,
6228                 )
6229             assertThat(eventLogBottomBox)
6230                 .containsExactly(
6231                     PointerEventType.Enter,
6232                     PointerEventType.Press,
6233                     PointerEventType.Release,
6234                 )
6235         }
6236 
6237         // Still a direct hit on bottom child box, so it wins.
6238         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords, Offset(0f, -dpInPixel!!))
6239         dispatchMouseEvent(ACTION_UP, parentBoxCoords, Offset(0f, -dpInPixel!!))
6240 
6241         rule.runOnUiThread {
6242             assertThat(eventLogTopBox)
6243                 .containsExactly(
6244                     PointerEventType.Press,
6245                     PointerEventType.Release,
6246                     PointerEventType.Press,
6247                     PointerEventType.Release,
6248                 )
6249             assertThat(eventLogBottomBox)
6250                 .containsExactly(
6251                     PointerEventType.Enter,
6252                     PointerEventType.Press,
6253                     PointerEventType.Release,
6254                     PointerEventType.Press,
6255                     PointerEventType.Release,
6256                 )
6257         }
6258 
6259         // Direct hit of edge of bottom parent box (and child box).
6260         dispatchMouseEvent(ACTION_DOWN, parentBoxCoords)
6261         dispatchMouseEvent(ACTION_UP, parentBoxCoords)
6262 
6263         rule.runOnUiThread {
6264             assertThat(eventLogTopBox)
6265                 .containsExactly(
6266                     PointerEventType.Press,
6267                     PointerEventType.Release,
6268                     PointerEventType.Press,
6269                     PointerEventType.Release,
6270                 )
6271             assertThat(eventLogBottomBox)
6272                 .containsExactly(
6273                     PointerEventType.Enter,
6274                     PointerEventType.Press,
6275                     PointerEventType.Release,
6276                     PointerEventType.Press,
6277                     PointerEventType.Release,
6278                     PointerEventType.Press,
6279                     PointerEventType.Release,
6280                 )
6281         }
6282     }
6283 
6284     /**
6285      * Touch events occur between two boxes that are both less than the minimum touch target size.
6286      * Tests an event directly between two boxes, then on each side of that border to make sure the
6287      * correct box event handlers are triggered.
6288      */
6289     @Test
6290     fun twoMinimumTouchTargetsAdjacent_nonDirectHitsBetweenTwo_triggersAppropriateBox() {
6291         var containingColumnPointerEventCount = 0
6292         var topBoxPointerEventCount = 0
6293         var bottomBoxPointerEventCount = 0
6294 
6295         val spaceBetweenElementsInColumnDp: Dp = 8.dp
6296         var spaceBetweenElementsInColumnPixel: Float? = null
6297         var dpInPixel: Float? = null
6298 
6299         var bottomBoxCoordinates: LayoutCoordinates? = null
6300         val latch = CountDownLatch(1)
6301         rule.runOnUiThread {
6302             container.setContent {
6303                 with(LocalDensity.current) {
6304                     dpInPixel = 1.dp.toPx()
6305                     spaceBetweenElementsInColumnPixel = spaceBetweenElementsInColumnDp.toPx()
6306                 }
6307                 Column(
6308                     Modifier.fillMaxSize().background(Color.Red).pointerInput(Unit) {
6309                         awaitPointerEventScope {
6310                             while (true) {
6311                                 awaitPointerEvent()
6312                                 containingColumnPointerEventCount++
6313                             }
6314                         }
6315                     },
6316                     verticalArrangement = Arrangement.spacedBy(spaceBetweenElementsInColumnDp)
6317                 ) {
6318                     Box(
6319                         Modifier.size(40.dp) // Below minimum touch target 48.dp
6320                             .background(Color.Green)
6321                             .pointerInput(Unit) {
6322                                 awaitPointerEventScope {
6323                                     while (true) {
6324                                         awaitPointerEvent()
6325                                         topBoxPointerEventCount++
6326                                     }
6327                                 }
6328                             }
6329                     )
6330                     Box(
6331                         Modifier.size(40.dp) // Below minimum touch target 48.dp
6332                             .background(Color.Cyan)
6333                             .pointerInput(Unit) {
6334                                 awaitPointerEventScope {
6335                                     while (true) {
6336                                         awaitPointerEvent()
6337                                         bottomBoxPointerEventCount++
6338                                     }
6339                                 }
6340                             }
6341                             .onGloballyPositioned {
6342                                 bottomBoxCoordinates = it
6343                                 latch.countDown()
6344                             }
6345                     )
6346                 }
6347             }
6348         }
6349         assertTrue(latch.await(1, TimeUnit.SECONDS))
6350         dispatchTouchEvent(ACTION_DOWN, bottomBoxCoordinates!!)
6351         dispatchTouchEvent(ACTION_UP, bottomBoxCoordinates!!)
6352 
6353         rule.runOnUiThread {
6354             assertThat(containingColumnPointerEventCount).isEqualTo(2)
6355             assertThat(topBoxPointerEventCount).isEqualTo(0)
6356             assertThat(bottomBoxPointerEventCount).isEqualTo(2)
6357         }
6358 
6359         val halfSpacing: Float = spaceBetweenElementsInColumnPixel!! / 2
6360         val negativeHalfSpaceTriggersBottomBox = -halfSpacing
6361         val negativeHalfSpaceMinusOnePixelTriggersTopBox =
6362             negativeHalfSpaceTriggersBottomBox - dpInPixel!!
6363 
6364         dispatchTouchEvent(
6365             ACTION_DOWN,
6366             bottomBoxCoordinates!!,
6367             Offset(0f, negativeHalfSpaceTriggersBottomBox)
6368         )
6369         dispatchTouchEvent(
6370             ACTION_UP,
6371             bottomBoxCoordinates!!,
6372             Offset(0f, negativeHalfSpaceTriggersBottomBox)
6373         )
6374 
6375         rule.runOnUiThread {
6376             assertThat(containingColumnPointerEventCount).isEqualTo(4)
6377             assertThat(topBoxPointerEventCount).isEqualTo(0)
6378             assertThat(bottomBoxPointerEventCount).isEqualTo(4)
6379         }
6380 
6381         dispatchTouchEvent(
6382             ACTION_DOWN,
6383             bottomBoxCoordinates!!,
6384             Offset(0f, negativeHalfSpaceMinusOnePixelTriggersTopBox)
6385         )
6386         dispatchTouchEvent(
6387             ACTION_UP,
6388             bottomBoxCoordinates!!,
6389             Offset(0f, negativeHalfSpaceMinusOnePixelTriggersTopBox)
6390         )
6391 
6392         rule.runOnUiThread {
6393             assertThat(containingColumnPointerEventCount).isEqualTo(6)
6394             assertThat(topBoxPointerEventCount).isEqualTo(2)
6395             assertThat(bottomBoxPointerEventCount).isEqualTo(4)
6396         }
6397     }
6398 
6399     /**
6400      * Tests overlapping siblings using a containing parent Box. The one on top (and that wins) is
6401      * determined by order.
6402      */
6403     @Test
6404     fun hitOnOverlappingSiblings_rightSiblingAbove_triggersRightSibling() {
6405         var containingBoxPointerEventCount = 0
6406         var leftBoxPointerEventCount = 0
6407         var rightBoxPointerEventCount = 0
6408         var rightBoxInnerChildPointerEventCount = 0
6409 
6410         var rightBoxInnerChild: LayoutCoordinates? = null
6411         val latch = CountDownLatch(1)
6412         rule.runOnUiThread {
6413             container.setContent {
6414                 Box(
6415                     Modifier.size(200.dp).background(Color.Red).pointerInput(Unit) {
6416                         awaitPointerEventScope {
6417                             while (true) {
6418                                 awaitPointerEvent()
6419                                 containingBoxPointerEventCount++
6420                             }
6421                         }
6422                     }
6423                 ) {
6424                     Box(
6425                         Modifier.width(180.dp)
6426                             .height(50.dp)
6427                             .padding(10.dp)
6428                             .background(Color.Green)
6429                             .align(Alignment.TopStart)
6430                             .pointerInput(Unit) {
6431                                 awaitPointerEventScope {
6432                                     while (true) {
6433                                         awaitPointerEvent()
6434                                         leftBoxPointerEventCount++
6435                                     }
6436                                 }
6437                             }
6438                     )
6439                     Box(
6440                         Modifier.size(100.dp)
6441                             .background(Color.Blue)
6442                             .align(Alignment.TopEnd)
6443                             .pointerInput(Unit) {
6444                                 awaitPointerEventScope {
6445                                     while (true) {
6446                                         awaitPointerEvent()
6447                                         rightBoxPointerEventCount++
6448                                     }
6449                                 }
6450                             }
6451                     ) {
6452                         Box(
6453                             Modifier.padding(10.dp)
6454                                 .size(80.dp)
6455                                 .background(Color.Cyan)
6456                                 .pointerInput(Unit) {
6457                                     awaitPointerEventScope {
6458                                         while (true) {
6459                                             awaitPointerEvent()
6460                                             rightBoxInnerChildPointerEventCount++
6461                                         }
6462                                     }
6463                                 }
6464                                 .onGloballyPositioned {
6465                                     rightBoxInnerChild = it
6466                                     latch.countDown()
6467                                 }
6468                         )
6469                     }
6470                 }
6471             }
6472         }
6473         assertTrue(latch.await(1, TimeUnit.SECONDS))
6474 
6475         dispatchTouchEvent(ACTION_DOWN, rightBoxInnerChild!!)
6476         dispatchTouchEvent(ACTION_UP, rightBoxInnerChild!!)
6477 
6478         rule.runOnUiThread {
6479             assertThat(containingBoxPointerEventCount).isEqualTo(2)
6480             assertThat(leftBoxPointerEventCount).isEqualTo(0)
6481             assertThat(rightBoxPointerEventCount).isEqualTo(2)
6482             assertThat(rightBoxInnerChildPointerEventCount).isEqualTo(2)
6483         }
6484     }
6485 
6486     /**
6487      * This test is exactly the same as
6488      * [hitOnOverlappingSiblings_rightSiblingAbove_triggersRightSibling], but with the z-index
6489      * manually changed on the overlapping sibling so it is above instead of below the other
6490      * conflicting sibling (so left sibling is triggered instead of right sibling).
6491      */
6492     @Test
6493     fun hitOnOverlappingSiblings_leftSiblingAboveViaZIndex_triggersLeftSibling() {
6494         var containingBoxPointerEventCount = 0
6495         var leftBoxPointerEventCount = 0
6496         var rightBoxPointerEventCount = 0
6497         var rightBoxInnerChildPointerEventCount = 0
6498 
6499         var rightBoxInnerChild: LayoutCoordinates? = null
6500         val latch = CountDownLatch(1)
6501         rule.runOnUiThread {
6502             container.setContent {
6503                 Box(
6504                     Modifier.size(200.dp).background(Color.Red).pointerInput(Unit) {
6505                         awaitPointerEventScope {
6506                             while (true) {
6507                                 awaitPointerEvent()
6508                                 containingBoxPointerEventCount++
6509                             }
6510                         }
6511                     }
6512                 ) {
6513                     Box(
6514                         Modifier.width(180.dp)
6515                             .height(50.dp)
6516                             .padding(10.dp)
6517                             .background(Color.Green)
6518                             .zIndex(1f)
6519                             .align(Alignment.TopStart)
6520                             .pointerInput(Unit) {
6521                                 awaitPointerEventScope {
6522                                     while (true) {
6523                                         awaitPointerEvent()
6524                                         leftBoxPointerEventCount++
6525                                     }
6526                                 }
6527                             }
6528                     )
6529                     Box(
6530                         Modifier.size(100.dp)
6531                             .background(Color.Blue)
6532                             .align(Alignment.TopEnd)
6533                             .pointerInput(Unit) {
6534                                 awaitPointerEventScope {
6535                                     while (true) {
6536                                         awaitPointerEvent()
6537                                         rightBoxPointerEventCount++
6538                                     }
6539                                 }
6540                             }
6541                     ) {
6542                         Box(
6543                             Modifier.padding(10.dp)
6544                                 .size(80.dp)
6545                                 .background(Color.Cyan)
6546                                 .pointerInput(Unit) {
6547                                     awaitPointerEventScope {
6548                                         while (true) {
6549                                             awaitPointerEvent()
6550                                             rightBoxInnerChildPointerEventCount++
6551                                         }
6552                                     }
6553                                 }
6554                                 .onGloballyPositioned {
6555                                     rightBoxInnerChild = it
6556                                     latch.countDown()
6557                                 }
6558                         )
6559                     }
6560                 }
6561             }
6562         }
6563         assertTrue(latch.await(1, TimeUnit.SECONDS))
6564 
6565         dispatchTouchEvent(ACTION_DOWN, rightBoxInnerChild!!)
6566         dispatchTouchEvent(ACTION_UP, rightBoxInnerChild!!)
6567 
6568         rule.runOnUiThread {
6569             assertThat(containingBoxPointerEventCount).isEqualTo(2)
6570             assertThat(leftBoxPointerEventCount).isEqualTo(2)
6571             assertThat(rightBoxPointerEventCount).isEqualTo(0)
6572             assertThat(rightBoxInnerChildPointerEventCount).isEqualTo(0)
6573         }
6574     }
6575 
6576     /**
6577      * This test uses the same UI elements as
6578      * [hitOnOverlappingSiblings_rightSiblingAbove_triggersRightSibling], but they are declared in
6579      * reverse order, so we can test the correct overlapping sibling (one above the other is
6580      * triggered).
6581      */
6582     @Test
6583     fun hitOnOverlappingSiblingsReversedUI_rightSiblingAbove_triggersRightSibling() {
6584         var containingBoxPointerEventCount = 0
6585         var leftBoxPointerEventCount = 0
6586         var leftBoxInnerChildPointerEventCount = 0
6587         var rightBoxPointerEventCount = 0
6588 
6589         var rightBoxInnerChild: LayoutCoordinates? = null
6590         val latch = CountDownLatch(1)
6591         rule.runOnUiThread {
6592             container.setContent {
6593                 Box(
6594                     Modifier.size(200.dp).background(Color.Red).pointerInput(Unit) {
6595                         awaitPointerEventScope {
6596                             while (true) {
6597                                 awaitPointerEvent()
6598                                 containingBoxPointerEventCount++
6599                             }
6600                         }
6601                     }
6602                 ) {
6603                     Box(
6604                         Modifier.size(100.dp)
6605                             .background(Color.Blue)
6606                             .align(Alignment.TopStart)
6607                             .pointerInput(Unit) {
6608                                 awaitPointerEventScope {
6609                                     while (true) {
6610                                         awaitPointerEvent()
6611                                         leftBoxPointerEventCount++
6612                                     }
6613                                 }
6614                             }
6615                     ) {
6616                         Box(
6617                             Modifier.padding(10.dp)
6618                                 .size(80.dp)
6619                                 .background(Color.Cyan)
6620                                 .pointerInput(Unit) {
6621                                     awaitPointerEventScope {
6622                                         while (true) {
6623                                             awaitPointerEvent()
6624                                             leftBoxInnerChildPointerEventCount++
6625                                         }
6626                                     }
6627                                 }
6628                                 .onGloballyPositioned {
6629                                     rightBoxInnerChild = it
6630                                     latch.countDown()
6631                                 }
6632                         )
6633                     }
6634 
6635                     Box(
6636                         Modifier.width(200.dp)
6637                             .height(50.dp)
6638                             .padding(10.dp)
6639                             .background(Color.Green)
6640                             .align(Alignment.TopEnd)
6641                             .pointerInput(Unit) {
6642                                 awaitPointerEventScope {
6643                                     while (true) {
6644                                         awaitPointerEvent()
6645                                         rightBoxPointerEventCount++
6646                                     }
6647                                 }
6648                             }
6649                     )
6650                 }
6651             }
6652         }
6653         assertTrue(latch.await(1, TimeUnit.SECONDS))
6654 
6655         dispatchTouchEvent(ACTION_DOWN, rightBoxInnerChild!!)
6656         dispatchTouchEvent(ACTION_UP, rightBoxInnerChild!!)
6657 
6658         rule.runOnUiThread {
6659             assertThat(containingBoxPointerEventCount).isEqualTo(2)
6660             assertThat(rightBoxPointerEventCount).isEqualTo(2)
6661             assertThat(leftBoxPointerEventCount).isEqualTo(0)
6662             assertThat(leftBoxInnerChildPointerEventCount).isEqualTo(0)
6663         }
6664     }
6665 
6666     /**
6667      * This test is exactly the same as
6668      * [hitOnOverlappingSiblingsReversedUI_rightSiblingAbove_triggersRightSibling], but with the
6669      * z-index manually changed on the overlapping sibling so it is above instead of below the other
6670      * conflicting sibling (so left sibling is triggered instead of right sibling).
6671      */
6672     @Test
6673     fun hitOnOverlappingSiblingsReversedUI_leftSiblingAboveViaZIndex_triggersLeftSibling() {
6674         var containingBoxPointerEventCount = 0
6675         var leftBoxPointerEventCount = 0
6676         var leftBoxInnerChildPointerEventCount = 0
6677         var rightBoxPointerEventCount = 0
6678 
6679         var rightBoxInnerChild: LayoutCoordinates? = null
6680         val latch = CountDownLatch(1)
6681         rule.runOnUiThread {
6682             container.setContent {
6683                 Box(
6684                     Modifier.size(200.dp).background(Color.Red).pointerInput(Unit) {
6685                         awaitPointerEventScope {
6686                             while (true) {
6687                                 awaitPointerEvent()
6688                                 containingBoxPointerEventCount++
6689                             }
6690                         }
6691                     }
6692                 ) {
6693                     Box(
6694                         Modifier.size(100.dp)
6695                             .background(Color.Blue)
6696                             .align(Alignment.TopStart)
6697                             .zIndex(1f)
6698                             .pointerInput(Unit) {
6699                                 awaitPointerEventScope {
6700                                     while (true) {
6701                                         awaitPointerEvent()
6702                                         leftBoxPointerEventCount++
6703                                     }
6704                                 }
6705                             }
6706                     ) {
6707                         Box(
6708                             Modifier.padding(10.dp)
6709                                 .size(80.dp)
6710                                 .background(Color.Cyan)
6711                                 .pointerInput(Unit) {
6712                                     awaitPointerEventScope {
6713                                         while (true) {
6714                                             awaitPointerEvent()
6715                                             leftBoxInnerChildPointerEventCount++
6716                                         }
6717                                     }
6718                                 }
6719                                 .onGloballyPositioned {
6720                                     rightBoxInnerChild = it
6721                                     latch.countDown()
6722                                 }
6723                         )
6724                     }
6725 
6726                     Box(
6727                         Modifier.width(200.dp)
6728                             .height(50.dp)
6729                             .padding(10.dp)
6730                             .background(Color.Green)
6731                             .align(Alignment.TopEnd)
6732                             .pointerInput(Unit) {
6733                                 awaitPointerEventScope {
6734                                     while (true) {
6735                                         awaitPointerEvent()
6736                                         rightBoxPointerEventCount++
6737                                     }
6738                                 }
6739                             }
6740                     )
6741                 }
6742             }
6743         }
6744         assertTrue(latch.await(1, TimeUnit.SECONDS))
6745 
6746         dispatchTouchEvent(ACTION_DOWN, rightBoxInnerChild!!)
6747         dispatchTouchEvent(ACTION_UP, rightBoxInnerChild!!)
6748 
6749         rule.runOnUiThread {
6750             assertThat(containingBoxPointerEventCount).isEqualTo(2)
6751             assertThat(rightBoxPointerEventCount).isEqualTo(0)
6752             assertThat(leftBoxPointerEventCount).isEqualTo(2)
6753             assertThat(leftBoxInnerChildPointerEventCount).isEqualTo(2)
6754         }
6755     }
6756 
6757     /**
6758      * This is similar to
6759      * [hitOnOverlappingSiblingsReversedUI_leftSiblingAboveViaZIndex_triggersLeftSibling] but
6760      * instead of setting the z-index of a sibling, we are setting the z-index of the child of the
6761      * sibling. Because z-index only influences siblings (and not parents, grandparents, etc.), this
6762      * will not changing the order of the left sibling and the right sibling will still win (since
6763      * it's UI was declared later).
6764      */
6765     @Test
6766     fun hitOnOverlappingSiblingsReversedUI_leftSiblingChildHighZIndex_triggersRightSibling() {
6767         var containingBoxPointerEventCount = 0
6768         var leftBoxPointerEventCount = 0
6769         var leftBoxInnerChildPointerEventCount = 0
6770         var rightBoxPointerEventCount = 0
6771 
6772         var rightBoxInnerChild: LayoutCoordinates? = null
6773         val latch = CountDownLatch(1)
6774         rule.runOnUiThread {
6775             container.setContent {
6776                 Box(
6777                     Modifier.size(200.dp).background(Color.Red).pointerInput(Unit) {
6778                         awaitPointerEventScope {
6779                             while (true) {
6780                                 awaitPointerEvent()
6781                                 containingBoxPointerEventCount++
6782                             }
6783                         }
6784                     }
6785                 ) {
6786                     Box(
6787                         Modifier.size(100.dp)
6788                             .background(Color.Blue)
6789                             .align(Alignment.TopStart)
6790                             .pointerInput(Unit) {
6791                                 awaitPointerEventScope {
6792                                     while (true) {
6793                                         awaitPointerEvent()
6794                                         leftBoxPointerEventCount++
6795                                     }
6796                                 }
6797                             }
6798                     ) {
6799                         Box(
6800                             Modifier.padding(10.dp)
6801                                 .size(80.dp)
6802                                 .background(Color.Cyan)
6803                                 .zIndex(1f)
6804                                 .pointerInput(Unit) {
6805                                     awaitPointerEventScope {
6806                                         while (true) {
6807                                             awaitPointerEvent()
6808                                             leftBoxInnerChildPointerEventCount++
6809                                         }
6810                                     }
6811                                 }
6812                                 .onGloballyPositioned {
6813                                     rightBoxInnerChild = it
6814                                     latch.countDown()
6815                                 }
6816                         )
6817                     }
6818 
6819                     Box(
6820                         Modifier.width(200.dp)
6821                             .height(50.dp)
6822                             .padding(10.dp)
6823                             .background(Color.Green)
6824                             .align(Alignment.TopEnd)
6825                             .pointerInput(Unit) {
6826                                 awaitPointerEventScope {
6827                                     while (true) {
6828                                         awaitPointerEvent()
6829                                         rightBoxPointerEventCount++
6830                                     }
6831                                 }
6832                             }
6833                     )
6834                 }
6835             }
6836         }
6837         assertTrue(latch.await(1, TimeUnit.SECONDS))
6838 
6839         dispatchTouchEvent(ACTION_DOWN, rightBoxInnerChild!!)
6840         dispatchTouchEvent(ACTION_UP, rightBoxInnerChild!!)
6841 
6842         rule.runOnUiThread {
6843             assertThat(containingBoxPointerEventCount).isEqualTo(2)
6844             assertThat(rightBoxPointerEventCount).isEqualTo(2)
6845             assertThat(leftBoxPointerEventCount).isEqualTo(0)
6846             assertThat(leftBoxInnerChildPointerEventCount).isEqualTo(0)
6847         }
6848     }
6849 
6850     @Test
6851     fun stylusEnterExitPointerArea() {
6852         // Stylus hover enter/exit events should be sent to pointer input areas
6853         val eventLog = mutableListOf<PointerEvent>()
6854         var innerCoordinates: LayoutCoordinates? = null
6855         val latch = CountDownLatch(1)
6856         rule.runOnUiThread {
6857             container.setContent {
6858                 Box(
6859                     Modifier.fillMaxSize().pointerInput(Unit) {
6860                         awaitPointerEventScope {
6861                             while (true) {
6862                                 awaitPointerEvent()
6863                             }
6864                         }
6865                     }
6866                 ) {
6867                     Box(
6868                         Modifier.size(50.dp)
6869                             .align(AbsoluteAlignment.BottomRight)
6870                             .pointerInput(Unit) {
6871                                 awaitPointerEventScope {
6872                                     while (true) {
6873                                         val event = awaitPointerEvent()
6874                                         event.changes.forEach { it.consume() }
6875                                         eventLog += event
6876                                     }
6877                                 }
6878                             }
6879                             .onGloballyPositioned {
6880                                 innerCoordinates = it
6881                                 latch.countDown()
6882                             }
6883                     )
6884                 }
6885             }
6886         }
6887         assertTrue(latch.await(1, TimeUnit.SECONDS))
6888 
6889         val coords = innerCoordinates!!
6890         val outside = Offset(-100f, -100f)
6891         dispatchStylusEvents(coords, outside, ACTION_HOVER_ENTER)
6892         rule.runOnUiThread {
6893             // The event didn't land inside the box, so it shouldn't get the hover enter
6894             assertThat(eventLog).isEmpty()
6895         }
6896         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_MOVE)
6897         dispatchStylusEvents(coords, Offset.Zero, ACTION_HOVER_EXIT, ACTION_DOWN)
6898         dispatchStylusEvents(coords, outside, ACTION_MOVE)
6899         dispatchStylusEvents(coords, outside, ACTION_UP, ACTION_HOVER_ENTER)
6900         rule.runOnUiThread {
6901             assertThat(eventLog).hasSize(4)
6902             assertThat(eventLog[0].type).isEqualTo(PointerEventType.Enter)
6903             assertThat(eventLog[1].type).isEqualTo(PointerEventType.Press)
6904             assertThat(eventLog[2].type).isEqualTo(PointerEventType.Exit)
6905             assertThat(eventLog[3].type).isEqualTo(PointerEventType.Release)
6906         }
6907     }
6908 
6909     @Test
6910     fun restartStreamAfterNotProcessing() {
6911         // Stylus hover enter/exit events should be sent to pointer input areas
6912         val eventLog = mutableListOf<PointerEvent>()
6913         var hitCoordinates: LayoutCoordinates? = null
6914         var missCoordinates: LayoutCoordinates? = null
6915         val latch = CountDownLatch(2)
6916         rule.runOnUiThread {
6917             container.setContent {
6918                 Box(Modifier.fillMaxSize()) {
6919                     Box(
6920                         Modifier.size(50.dp)
6921                             .align(AbsoluteAlignment.TopLeft)
6922                             .pointerInput(Unit) {
6923                                 awaitPointerEventScope {
6924                                     while (true) {
6925                                         val event = awaitPointerEvent()
6926                                         event.changes.forEach { it.consume() }
6927                                         eventLog += event
6928                                     }
6929                                 }
6930                             }
6931                             .onGloballyPositioned {
6932                                 hitCoordinates = it
6933                                 latch.countDown()
6934                             }
6935                     )
6936                     Box(
6937                         Modifier.size(50.dp)
6938                             .align(AbsoluteAlignment.BottomRight)
6939                             .onGloballyPositioned {
6940                                 missCoordinates = it
6941                                 latch.countDown()
6942                             }
6943                     )
6944                 }
6945             }
6946         }
6947         assertTrue(latch.await(1, TimeUnit.SECONDS))
6948         val miss = missCoordinates!!
6949         val hit = hitCoordinates!!
6950 
6951         // This should hit
6952         dispatchTouchEvent(ACTION_DOWN, hit)
6953         dispatchTouchEvent(ACTION_UP, hit)
6954 
6955         // This should miss
6956         dispatchTouchEvent(ACTION_DOWN, miss)
6957 
6958         // This should hit
6959         dispatchTouchEvent(ACTION_DOWN, hit)
6960 
6961         rule.runOnUiThread {
6962             assertThat(eventLog).hasSize(3)
6963             val down1 = eventLog[0]
6964             val up1 = eventLog[1]
6965             val down2 = eventLog[2]
6966             assertThat(down1.changes).hasSize(1)
6967             assertThat(up1.changes).hasSize(1)
6968             assertThat(down2.changes).hasSize(1)
6969 
6970             assertThat(down1.type).isEqualTo(PointerEventType.Press)
6971             assertThat(up1.type).isEqualTo(PointerEventType.Release)
6972             assertThat(down2.type).isEqualTo(PointerEventType.Press)
6973 
6974             assertThat(up1.changes[0].id).isEqualTo(down1.changes[0].id)
6975             assertThat(down2.changes[0].id.value).isEqualTo(down1.changes[0].id.value + 2)
6976         }
6977     }
6978 
6979     @Test
6980     fun pointerEventGetMotionEventAfterDispatch_returnsNull() {
6981         val eventLog = mutableListOf<PointerEvent>()
6982         val latch = CountDownLatch(1)
6983         var layoutCoordinates: LayoutCoordinates? = null
6984         rule.runOnUiThread {
6985             container.setContent {
6986                 Box(
6987                     Modifier.fillMaxSize()
6988                         .pointerInput(Unit) {
6989                             awaitPointerEventScope {
6990                                 val pointerEvent1 = awaitPointerEvent()
6991                                 assertThat(pointerEvent1.motionEvent).isNotNull()
6992                                 eventLog.add(pointerEvent1)
6993 
6994                                 val pointerEvent2 = awaitPointerEvent()
6995                                 assertThat(pointerEvent2.motionEvent).isNotNull()
6996                                 assertThat(pointerEvent1.motionEvent).isNull()
6997                                 eventLog.add(pointerEvent2)
6998                             }
6999                         }
7000                         .onGloballyPositioned {
7001                             layoutCoordinates = it
7002                             latch.countDown()
7003                         }
7004                 )
7005             }
7006         }
7007         assertTrue(latch.await(1, TimeUnit.SECONDS))
7008 
7009         dispatchTouchEvent(action = ACTION_DOWN, layoutCoordinates = layoutCoordinates!!)
7010         dispatchTouchEvent(action = ACTION_UP, layoutCoordinates = layoutCoordinates!!)
7011 
7012         rule.waitForFutureFrame()
7013         // Gut check that the pointer events are correctly dispatched.
7014         rule.runOnUiThread { assertThat(eventLog).hasSize(2) }
7015     }
7016 
7017     private fun createPointerEventAt(eventTime: Int, action: Int, locationInWindow: IntArray) =
7018         MotionEvent(
7019             eventTime,
7020             action,
7021             1,
7022             0,
7023             arrayOf(PointerProperties(0)),
7024             arrayOf(PointerCoords(locationInWindow[0].toFloat(), locationInWindow[1].toFloat()))
7025         )
7026 }
7027 
7028 @Composable
AndroidWithComposenull7029 fun AndroidWithCompose(context: Context, androidPadding: Int, content: @Composable () -> Unit) {
7030     val anotherLayout =
7031         ComposeView(context).also { view ->
7032             view.setContent { content() }
7033             view.setPadding(androidPadding, androidPadding, androidPadding, androidPadding)
7034         }
7035     AndroidView({ anotherLayout })
7036 }
7037 
<lambda>null7038 fun Modifier.consumeMovementGestureFilter(consumeMovement: Boolean = false): Modifier = composed {
7039     val filter = remember(consumeMovement) { ConsumeMovementGestureFilter(consumeMovement) }
7040     PointerInputModifierImpl(filter)
7041 }
7042 
<lambda>null7043 fun Modifier.consumeDownGestureFilter(onDown: (Offset) -> Unit): Modifier = composed {
7044     val filter = remember { ConsumeDownChangeFilter() }
7045     filter.onDown = onDown
7046     this.then(PointerInputModifierImpl(filter))
7047 }
7048 
logEventsGestureFilternull7049 fun Modifier.logEventsGestureFilter(log: MutableList<List<PointerInputChange>>): Modifier =
7050     composed {
7051         val filter = remember { LogEventsGestureFilter(log) }
7052         this.then(PointerInputModifierImpl(filter))
7053     }
7054 
7055 private class PointerInputModifierImpl(override val pointerInputFilter: PointerInputFilter) :
7056     PointerInputModifier
7057 
7058 private class ConsumeMovementGestureFilter(val consumeMovement: Boolean) : PointerInputFilter() {
onPointerEventnull7059     override fun onPointerEvent(
7060         pointerEvent: PointerEvent,
7061         pass: PointerEventPass,
7062         bounds: IntSize
7063     ) {
7064         if (consumeMovement) {
7065             pointerEvent.changes.fastForEach { it.consume() }
7066         }
7067     }
7068 
onCancelnull7069     override fun onCancel() {}
7070 }
7071 
7072 private class ConsumeDownChangeFilter : PointerInputFilter() {
<lambda>null7073     var onDown by mutableStateOf<(Offset) -> Unit>({})
7074 
onPointerEventnull7075     override fun onPointerEvent(
7076         pointerEvent: PointerEvent,
7077         pass: PointerEventPass,
7078         bounds: IntSize
7079     ) {
7080         pointerEvent.changes.fastForEach {
7081             if (it.changedToDown()) {
7082                 onDown(it.position)
7083                 it.consume()
7084             }
7085         }
7086     }
7087 
onCancelnull7088     override fun onCancel() {}
7089 }
7090 
7091 private class LogEventsGestureFilter(val log: MutableList<List<PointerInputChange>>) :
7092     PointerInputFilter() {
7093 
onPointerEventnull7094     override fun onPointerEvent(
7095         pointerEvent: PointerEvent,
7096         pass: PointerEventPass,
7097         bounds: IntSize
7098     ) {
7099         if (pass == PointerEventPass.Initial) {
7100             log.add(pointerEvent.changes.map { it.copy() })
7101         }
7102     }
7103 
onCancelnull7104     override fun onCancel() {}
7105 }
7106 
7107 @Suppress("TestFunctionName")
7108 @Composable
FillLayoutnull7109 private fun FillLayout(modifier: Modifier = Modifier) {
7110     Layout({}, modifier) { _, constraints ->
7111         layout(constraints.maxWidth, constraints.maxHeight) {}
7112     }
7113 }
7114 
countDownnull7115 private fun countDown(block: (CountDownLatch) -> Unit) {
7116     val countDownLatch = CountDownLatch(1)
7117     block(countDownLatch)
7118     assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue()
7119 }
7120 
7121 class AndroidPointerInputTestActivity : ComponentActivity()
7122 
MotionEventnull7123 private fun MotionEvent(
7124     eventTime: Int,
7125     action: Int,
7126     numPointers: Int,
7127     actionIndex: Int,
7128     pointerProperties: Array<MotionEvent.PointerProperties>,
7129     pointerCoords: Array<MotionEvent.PointerCoords>,
7130     buttonState: Int =
7131         if (
7132             pointerProperties[0].toolType == TOOL_TYPE_MOUSE &&
7133                 (action == ACTION_DOWN || action == ACTION_MOVE)
7134         )
7135             MotionEvent.BUTTON_PRIMARY
7136         else 0,
7137 ): MotionEvent {
7138     val source =
7139         if (pointerProperties[0].toolType == TOOL_TYPE_MOUSE) {
7140             InputDevice.SOURCE_MOUSE
7141         } else {
7142             InputDevice.SOURCE_TOUCHSCREEN
7143         }
7144     return MotionEvent.obtain(
7145         0,
7146         eventTime.toLong(),
7147         action + (actionIndex shl ACTION_POINTER_INDEX_SHIFT),
7148         numPointers,
7149         pointerProperties,
7150         pointerCoords,
7151         0,
7152         buttonState,
7153         0f,
7154         0f,
7155         0,
7156         0,
7157         source,
7158         0
7159     )
7160 }
7161 
7162 /*
7163  * Version of MotionEvent() that accepts classification.
7164  */
7165 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
MotionEventnull7166 private fun MotionEvent(
7167     eventTime: Int,
7168     action: Int,
7169     numPointers: Int,
7170     actionIndex: Int,
7171     pointerProperties: Array<MotionEvent.PointerProperties>,
7172     pointerCoords: Array<MotionEvent.PointerCoords>,
7173     buttonState: Int =
7174         if (
7175             pointerProperties[0].toolType == TOOL_TYPE_MOUSE &&
7176                 (action == ACTION_DOWN || action == ACTION_MOVE)
7177         )
7178             MotionEvent.BUTTON_PRIMARY
7179         else 0,
7180     classification: Int
7181 ): MotionEvent {
7182     val source =
7183         if (pointerProperties[0].toolType == TOOL_TYPE_MOUSE) {
7184             InputDevice.SOURCE_MOUSE
7185         } else {
7186             InputDevice.SOURCE_TOUCHSCREEN
7187         }
7188     return MotionEvent.obtain(
7189         0,
7190         eventTime.toLong(),
7191         action + (actionIndex shl ACTION_POINTER_INDEX_SHIFT),
7192         numPointers,
7193         pointerProperties,
7194         pointerCoords,
7195         0,
7196         buttonState,
7197         0f,
7198         0f,
7199         0,
7200         0,
7201         source,
7202         0,
7203         0,
7204         classification
7205     )!!
7206 }
7207 
findRootViewnull7208 internal fun findRootView(view: View): View {
7209     val parent = view.parent
7210     if (parent is View) {
7211         return findRootView(parent)
7212     }
7213     return view
7214 }
7215