[go: nahoru, domu]

Add partial gesture injection API

This allows developers to inject gestures that can be interspersed
with assertions, and it allows them to inject custom gestures. A new
receiver scope is introduced next to GestureScope, PartialGestureScope,
which contains the following methods:
* sendDown
* sendMoveTo
* sendMoveBy
* sendUp
* sendCancel

Use the action `doPartialGesture` to execute your partial gestures.

Bug: 152477560
Test: Added new tests for PartialGestureScope methods.
Relnote: "Adds `doPartialGesture` action with PartialGestureScope
receiver that has the methods `sendDown`, `sendMoveTo`, `sendMoveBy`,
`sendUp` and `sendCancel`."

Change-Id: I6b05886b2a2e49ae79a131ac9b6dbb7bdb0fb907
diff --git a/ui/ui-test/src/androidTest/AndroidManifest.xml b/ui/ui-test/src/androidTest/AndroidManifest.xml
index 86cb9d3..7ac2dec 100644
--- a/ui/ui-test/src/androidTest/AndroidManifest.xml
+++ b/ui/ui-test/src/androidTest/AndroidManifest.xml
@@ -21,7 +21,7 @@
             android:theme="@style/TestTheme"/>
         <activity android:name="androidx.ui.test.CustomActivity"
             android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen"/>
-        <activity android:name="androidx.ui.test.ActivityWithActionBar" />
+        <activity android:name="androidx.ui.test.gesturescope.ActivityWithActionBar" />
         <activity android:name="androidx.ui.test.MultipleActivitiesFindTest$Activity1" />
         <activity android:name="androidx.ui.test.MultipleActivitiesFindTest$Activity2" />
         <activity android:name="androidx.ui.test.MultipleActivitiesClickTest$Activity1" />
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendClickTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendClickTest.kt
similarity index 97%
rename from ui/ui-test/src/androidTest/java/androidx/ui/test/SendClickTest.kt
rename to ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendClickTest.kt
index 143c07b..98b3c66 100644
--- a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendClickTest.kt
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendClickTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.ui.test
+package androidx.ui.test.gesturescope
 
 import android.os.Bundle
 import android.view.Gravity
@@ -38,6 +38,11 @@
 import androidx.ui.layout.preferredSize
 import androidx.ui.semantics.Semantics
 import androidx.ui.test.android.AndroidComposeTestRule
+import androidx.ui.test.doGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.runOnUiThread
+import androidx.ui.test.sendClick
 import androidx.ui.unit.IntPxSize
 import androidx.ui.unit.PxPosition
 import androidx.ui.unit.px
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendDoubleClickTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendDoubleClickTest.kt
similarity index 96%
rename from ui/ui-test/src/androidTest/java/androidx/ui/test/SendDoubleClickTest.kt
rename to ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendDoubleClickTest.kt
index b356638..401abcd 100644
--- a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendDoubleClickTest.kt
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendDoubleClickTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.ui.test
+package androidx.ui.test.gesturescope
 
 import androidx.compose.Composable
 import androidx.test.filters.MediumTest
@@ -29,6 +29,11 @@
 import androidx.ui.layout.preferredSize
 import androidx.ui.semantics.Semantics
 import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnUiThread
+import androidx.ui.test.sendDoubleClick
 import androidx.ui.test.util.PointerInputRecorder
 import androidx.ui.test.util.assertTimestampsAreIncreasing
 import androidx.ui.unit.Px
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendLongClickTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendLongClickTest.kt
similarity index 93%
rename from ui/ui-test/src/androidTest/java/androidx/ui/test/SendLongClickTest.kt
rename to ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendLongClickTest.kt
index e7b21a7..8366cc4 100644
--- a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendLongClickTest.kt
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendLongClickTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.ui.test
+package androidx.ui.test.gesturescope
 
 import androidx.compose.Composable
 import androidx.test.filters.MediumTest
@@ -22,8 +22,8 @@
 import androidx.ui.core.DensityAmbient
 import androidx.ui.core.Modifier
 import androidx.ui.core.TestTag
-import androidx.ui.core.gesture.longPressGestureFilter
 import androidx.ui.core.gesture.LongPressTimeout
+import androidx.ui.core.gesture.longPressGestureFilter
 import androidx.ui.foundation.Box
 import androidx.ui.graphics.Color
 import androidx.ui.layout.Stack
@@ -31,6 +31,11 @@
 import androidx.ui.layout.preferredSize
 import androidx.ui.layout.wrapContentSize
 import androidx.ui.semantics.Semantics
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnUiThread
+import androidx.ui.test.sendLongClick
 import androidx.ui.test.util.PointerInputRecorder
 import androidx.ui.test.util.areAlmostEqualTo
 import androidx.ui.test.util.assertOnlyLastEventIsUp
@@ -70,7 +75,7 @@
 }
 
 /**
- * Tests [GestureScope.sendLongClick] without arguments. Verifies that the click is in the middle
+ * Tests [sendLongClick] without arguments. Verifies that the click is in the middle
  * of the component, that the gesture has a duration of 600 milliseconds and that all input
  * events were on the same location.
  */
@@ -113,7 +118,7 @@
 }
 
 /**
- * Tests [GestureScope.sendLongClick] with arguments. Verifies that the click is in the middle
+ * Tests [sendLongClick] with arguments. Verifies that the click is in the middle
  * of the component, that the gesture has a duration of 600 milliseconds and that all input
  * events were on the same location.
  */
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendSwipeTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendSwipeTest.kt
similarity index 94%
rename from ui/ui-test/src/androidTest/java/androidx/ui/test/SendSwipeTest.kt
rename to ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendSwipeTest.kt
index 8bdb5e9..f966857 100644
--- a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendSwipeTest.kt
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendSwipeTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.ui.test
+package androidx.ui.test.gesturescope
 
 import androidx.compose.Composable
 import androidx.test.filters.MediumTest
@@ -28,6 +28,14 @@
 import androidx.ui.layout.wrapContentSize
 import androidx.ui.semantics.Semantics
 import androidx.ui.semantics.testTag
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnUiThread
+import androidx.ui.test.sendSwipeDown
+import androidx.ui.test.sendSwipeLeft
+import androidx.ui.test.sendSwipeRight
+import androidx.ui.test.sendSwipeUp
 import androidx.ui.test.util.PointerInputRecorder
 import androidx.ui.test.util.assertOnlyLastEventIsUp
 import androidx.ui.test.util.assertTimestampsAreIncreasing
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendSwipeVelocityTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt
similarity index 96%
rename from ui/ui-test/src/androidTest/java/androidx/ui/test/SendSwipeVelocityTest.kt
rename to ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt
index 7836d19..252823d 100644
--- a/ui/ui-test/src/androidTest/java/androidx/ui/test/SendSwipeVelocityTest.kt
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/gesturescope/SendSwipeVelocityTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.ui.test
+package androidx.ui.test.gesturescope
 
 import androidx.compose.Composable
 import androidx.compose.remember
@@ -33,6 +33,11 @@
 import androidx.ui.semantics.Semantics
 import androidx.ui.semantics.testTag
 import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnUiThread
+import androidx.ui.test.sendSwipeWithVelocity
 import androidx.ui.test.util.PointerInputRecorder
 import androidx.ui.test.util.assertOnlyLastEventIsUp
 import androidx.ui.test.util.assertTimestampsAreIncreasing
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendCancelTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendCancelTest.kt
new file mode 100644
index 0000000..b6c4ee7
--- /dev/null
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendCancelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.test.partialgesturescope
+
+import androidx.test.filters.MediumTest
+import androidx.ui.graphics.Color
+import androidx.ui.test.GestureToken
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doPartialGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.sendCancel
+import androidx.ui.test.sendDown
+import androidx.ui.test.util.ClickableTestBox
+import androidx.ui.test.util.PointerInputRecorder
+import androidx.ui.test.util.assertTimestampsAreIncreasing
+import androidx.ui.test.util.inMilliseconds
+import androidx.ui.unit.PxPosition
+import androidx.ui.unit.px
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private val width = 200.px
+private val height = 200.px
+
+private const val tag = "widget"
+
+@MediumTest
+@RunWith(Parameterized::class)
+class SendCancelTest(private val config: TestConfig) {
+    data class TestConfig(val cancelPosition: PxPosition?) {
+        val downPosition = PxPosition(1.px, 1.px)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return mutableListOf<TestConfig>().apply {
+                for (x in listOf(2.px, 99.px)) {
+                    for (y in listOf(3.px, 53.px)) {
+                        add(TestConfig(PxPosition(x, y)))
+                    }
+                }
+                add(TestConfig(null))
+            }
+        }
+    }
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val dispatcherRule = AndroidInputDispatcher.TestRule(disableDispatchInRealTime = true)
+    @get:Rule
+    val inputDispatcherRule: TestRule = dispatcherRule
+
+    private lateinit var recorder: PointerInputRecorder
+    private val expectedCancelPosition = config.cancelPosition ?: config.downPosition
+
+    @Test
+    fun testSendCancel() {
+        // Given some content
+        recorder = PointerInputRecorder()
+        composeTestRule.setContent {
+            ClickableTestBox(width, height, Color.Yellow, tag, recorder)
+        }
+
+        // When we inject a down event followed by a cancel event
+        lateinit var token: GestureToken
+        findByTag(tag).doPartialGesture { token = sendDown(config.downPosition) }
+        findByTag(tag).doPartialGesture { sendCancel(token, config.cancelPosition) }
+
+        runOnIdleCompose {
+            recorder.run {
+                // Then we have only recorded 1 down event
+                assertTimestampsAreIncreasing()
+                assertThat(events).hasSize(1)
+
+                // But the information in the token matches the cancel event
+                assertThat(token.downTime).isEqualTo(events[0].timestamp.inMilliseconds())
+                assertThat(token.eventTime)
+                    .isEqualTo(events[0].timestamp.inMilliseconds() + dispatcherRule.eventPeriod)
+                assertThat(token.lastPosition).isEqualTo(expectedCancelPosition)
+            }
+        }
+    }
+}
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendDownTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendDownTest.kt
new file mode 100644
index 0000000..c56472e
--- /dev/null
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendDownTest.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.test.partialgesturescope
+
+import androidx.test.filters.MediumTest
+import androidx.ui.graphics.Color
+import androidx.ui.test.GestureToken
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doPartialGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.sendDown
+import androidx.ui.test.util.ClickableTestBox
+import androidx.ui.test.util.PointerInputRecorder
+import androidx.ui.test.util.assertTimestampsAreIncreasing
+import androidx.ui.test.util.inMilliseconds
+import androidx.ui.unit.PxPosition
+import androidx.ui.unit.px
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private val width = 200.px
+private val height = 200.px
+
+private const val tag = "widget"
+
+@MediumTest
+@RunWith(Parameterized::class)
+class SendDownTest(private val config: TestConfig) {
+    data class TestConfig(val position: PxPosition)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return mutableListOf<TestConfig>().apply {
+                for (x in listOf(1.px, 99.px)) {
+                    for (y in listOf(2.px, 53.px)) {
+                        add(TestConfig(PxPosition(x, y)))
+                    }
+                }
+            }
+        }
+    }
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @get:Rule
+    val inputDispatcherRule: TestRule = AndroidInputDispatcher.TestRule(
+        disableDispatchInRealTime = true
+    )
+
+    private lateinit var recorder: PointerInputRecorder
+    private val expectedPosition = config.position
+
+    @Test
+    fun testSendDown() {
+        // Given some content
+        recorder = PointerInputRecorder()
+        composeTestRule.setContent {
+            ClickableTestBox(width, height, Color.Yellow, tag, recorder)
+        }
+
+        // When we inject a down event
+        lateinit var token: GestureToken
+        findByTag(tag).doPartialGesture { token = sendDown(config.position) }
+
+        runOnIdleCompose {
+            recorder.run {
+                // Then we have recorded 1 down event
+                assertTimestampsAreIncreasing()
+                assertThat(events).hasSize(1)
+                assertThat(events[0].down).isTrue()
+                assertThat(events[0].position).isEqualTo(expectedPosition)
+
+                // That matches the information in the token
+                assertThat(token.downTime).isEqualTo(events[0].timestamp.inMilliseconds())
+                assertThat(token.eventTime).isEqualTo(events[0].timestamp.inMilliseconds())
+                assertThat(token.lastPosition).isEqualTo(expectedPosition)
+            }
+        }
+    }
+}
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendMoveByTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendMoveByTest.kt
new file mode 100644
index 0000000..acc538a
--- /dev/null
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendMoveByTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.test.partialgesturescope
+
+import androidx.test.filters.MediumTest
+import androidx.ui.graphics.Color
+import androidx.ui.test.GestureToken
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doPartialGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.sendDown
+import androidx.ui.test.sendMoveBy
+import androidx.ui.test.util.ClickableTestBox
+import androidx.ui.test.util.PointerInputRecorder
+import androidx.ui.test.util.assertTimestampsAreIncreasing
+import androidx.ui.test.util.inMilliseconds
+import androidx.ui.unit.PxPosition
+import androidx.ui.unit.inMilliseconds
+import androidx.ui.unit.px
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private val width = 200.px
+private val height = 200.px
+
+private const val tag = "widget"
+
+@MediumTest
+@RunWith(Parameterized::class)
+class SendMoveByTest(private val config: TestConfig) {
+    data class TestConfig(val moveByDelta: PxPosition) {
+        val downPosition = PxPosition(1.px, 1.px)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return mutableListOf<TestConfig>().apply {
+                for (x in listOf(2.px, (-100).px)) {
+                    for (y in listOf(3.px, (-530).px)) {
+                        add(TestConfig(PxPosition(x, y)))
+                    }
+                }
+            }
+        }
+    }
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val dispatcherRule = AndroidInputDispatcher.TestRule(disableDispatchInRealTime = true)
+    @get:Rule
+    val inputDispatcherRule: TestRule = dispatcherRule
+
+    private lateinit var recorder: PointerInputRecorder
+    private val expectedEndPosition = config.downPosition + config.moveByDelta
+
+    @Test
+    fun testSendMoveBy() {
+        // Given some content
+        recorder = PointerInputRecorder()
+        composeTestRule.setContent {
+            ClickableTestBox(width, height, Color.Yellow, tag, recorder)
+        }
+
+        // When we inject a down event followed by a move event
+        lateinit var token: GestureToken
+        findByTag(tag).doPartialGesture { token = sendDown(config.downPosition) }
+        findByTag(tag).doPartialGesture { sendMoveBy(token, config.moveByDelta) }
+
+        runOnIdleCompose {
+            recorder.run {
+                // Then we have recorded 1 down event and 1 move event
+                assertTimestampsAreIncreasing()
+                assertThat(events).hasSize(2)
+                assertThat(events[1].down).isTrue()
+                assertThat(events[1].position).isEqualTo(expectedEndPosition)
+                assertThat((events[1].timestamp - events[0].timestamp).inMilliseconds())
+                    .isEqualTo(dispatcherRule.eventPeriod)
+
+                // And the information in the token matches the last move event
+                assertThat(token.downTime).isEqualTo(events[0].timestamp.inMilliseconds())
+                assertThat(token.eventTime).isEqualTo(events[1].timestamp.inMilliseconds())
+                assertThat(token.lastPosition).isEqualTo(expectedEndPosition)
+            }
+        }
+    }
+}
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendMoveToTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendMoveToTest.kt
new file mode 100644
index 0000000..86092f5
--- /dev/null
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendMoveToTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.test.partialgesturescope
+
+import androidx.test.filters.MediumTest
+import androidx.ui.graphics.Color
+import androidx.ui.test.GestureToken
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doPartialGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.sendDown
+import androidx.ui.test.sendMoveTo
+import androidx.ui.test.util.ClickableTestBox
+import androidx.ui.test.util.PointerInputRecorder
+import androidx.ui.test.util.assertTimestampsAreIncreasing
+import androidx.ui.test.util.inMilliseconds
+import androidx.ui.unit.PxPosition
+import androidx.ui.unit.inMilliseconds
+import androidx.ui.unit.px
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private val width = 200.px
+private val height = 200.px
+
+private const val tag = "widget"
+
+@MediumTest
+@RunWith(Parameterized::class)
+class SendMoveToTest(private val config: TestConfig) {
+    data class TestConfig(val moveToPosition: PxPosition) {
+        val downPosition = PxPosition(1.px, 1.px)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return mutableListOf<TestConfig>().apply {
+                for (x in listOf(2.px, 99.px)) {
+                    for (y in listOf(3.px, 53.px)) {
+                        add(TestConfig(PxPosition(x, y)))
+                    }
+                }
+            }
+        }
+    }
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val dispatcherRule = AndroidInputDispatcher.TestRule(disableDispatchInRealTime = true)
+    @get:Rule
+    val inputDispatcherRule: TestRule = dispatcherRule
+
+    private lateinit var recorder: PointerInputRecorder
+    private val expectedEndPosition = config.moveToPosition
+
+    @Test
+    fun testSendMoveTo() {
+        // Given some content
+        recorder = PointerInputRecorder()
+        composeTestRule.setContent {
+            ClickableTestBox(width, height, Color.Yellow, tag, recorder)
+        }
+
+        // When we inject a down event followed by a move event
+        lateinit var token: GestureToken
+        findByTag(tag).doPartialGesture { token = sendDown(config.downPosition) }
+        findByTag(tag).doPartialGesture { sendMoveTo(token, config.moveToPosition) }
+
+        runOnIdleCompose {
+            recorder.run {
+                // Then we have recorded 1 down event and 1 move event
+                assertTimestampsAreIncreasing()
+                assertThat(events).hasSize(2)
+                assertThat(events[1].down).isTrue()
+                assertThat(events[1].position).isEqualTo(expectedEndPosition)
+                assertThat((events[1].timestamp - events[0].timestamp).inMilliseconds())
+                    .isEqualTo(dispatcherRule.eventPeriod)
+
+                // And the information in the token matches the last move event
+                assertThat(token.downTime).isEqualTo(events[0].timestamp.inMilliseconds())
+                assertThat(token.eventTime).isEqualTo(events[1].timestamp.inMilliseconds())
+                assertThat(token.lastPosition).isEqualTo(expectedEndPosition)
+            }
+        }
+    }
+}
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendUpTest.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendUpTest.kt
new file mode 100644
index 0000000..6bc448b
--- /dev/null
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/partialgesturescope/SendUpTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.test.partialgesturescope
+
+import androidx.test.filters.MediumTest
+import androidx.ui.graphics.Color
+import androidx.ui.test.GestureToken
+import androidx.ui.test.android.AndroidInputDispatcher
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.doPartialGesture
+import androidx.ui.test.findByTag
+import androidx.ui.test.runOnIdleCompose
+import androidx.ui.test.sendDown
+import androidx.ui.test.sendUp
+import androidx.ui.test.util.ClickableTestBox
+import androidx.ui.test.util.PointerInputRecorder
+import androidx.ui.test.util.assertTimestampsAreIncreasing
+import androidx.ui.test.util.inMilliseconds
+import androidx.ui.unit.PxPosition
+import androidx.ui.unit.inMilliseconds
+import androidx.ui.unit.px
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private val width = 200.px
+private val height = 200.px
+
+private const val tag = "widget"
+
+@MediumTest
+@RunWith(Parameterized::class)
+class SendUpTest(private val config: TestConfig) {
+    data class TestConfig(val upPosition: PxPosition?) {
+        val downPosition: PxPosition = PxPosition(1.px, 1.px)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return mutableListOf<TestConfig>().apply {
+                for (x in listOf(2.px, 99.px)) {
+                    for (y in listOf(3.px, 53.px)) {
+                        add(TestConfig(PxPosition(x, y)))
+                    }
+                }
+                add(TestConfig(null))
+            }
+        }
+    }
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val dispatcherRule = AndroidInputDispatcher.TestRule(disableDispatchInRealTime = true)
+    @get:Rule
+    val inputDispatcherRule: TestRule = dispatcherRule
+
+    private lateinit var recorder: PointerInputRecorder
+    private val expectedEndPosition = config.upPosition ?: config.downPosition
+
+    @Test
+    fun testSendUp() {
+        // Given some content
+        recorder = PointerInputRecorder()
+        composeTestRule.setContent {
+            ClickableTestBox(width, height, Color.Yellow, tag, recorder)
+        }
+
+        // When we inject a down event followed by an up event
+        lateinit var token: GestureToken
+        findByTag(tag).doPartialGesture { token = sendDown(config.downPosition) }
+        findByTag(tag).doPartialGesture { sendUp(token, config.upPosition) }
+
+        runOnIdleCompose {
+            recorder.run {
+                // Then we have recorded 1 down event and 1 move event
+                assertTimestampsAreIncreasing()
+                assertThat(events).hasSize(2)
+                assertThat(events[1].down).isFalse()
+                assertThat(events[1].position).isEqualTo(expectedEndPosition)
+                assertThat((events[1].timestamp - events[0].timestamp).inMilliseconds())
+                    .isEqualTo(dispatcherRule.eventPeriod)
+
+                // And the information in the token matches the last move event
+                assertThat(token.downTime).isEqualTo(events[0].timestamp.inMilliseconds())
+                assertThat(token.eventTime).isEqualTo(events[1].timestamp.inMilliseconds())
+                assertThat(token.lastPosition).isEqualTo(expectedEndPosition)
+            }
+        }
+    }
+}
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/util/ClickableTestBox.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/util/ClickableTestBox.kt
new file mode 100644
index 0000000..ec46184
--- /dev/null
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/util/ClickableTestBox.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.test.util
+
+import androidx.compose.Composable
+import androidx.ui.core.DensityAmbient
+import androidx.ui.core.pointerinput.PointerInputModifier
+import androidx.ui.foundation.Box
+import androidx.ui.graphics.Color
+import androidx.ui.layout.size
+import androidx.ui.semantics.Semantics
+import androidx.ui.semantics.testTag
+import androidx.ui.unit.Px
+
+@Composable
+fun ClickableTestBox(
+    width: Px,
+    height: Px,
+    color: Color,
+    tag: String,
+    pointerInputModifier: PointerInputModifier
+) {
+    Semantics(container = true, properties = { testTag = tag }) {
+        with(DensityAmbient.current) {
+            Box(
+                modifier = pointerInputModifier.size(width.toDp(), height.toDp()),
+                backgroundColor = color
+            )
+        }
+    }
+}
diff --git a/ui/ui-test/src/androidTest/java/androidx/ui/test/util/PointerInputs.kt b/ui/ui-test/src/androidTest/java/androidx/ui/test/util/PointerInputs.kt
index 3c25ab5..2321adc 100644
--- a/ui/ui-test/src/androidTest/java/androidx/ui/test/util/PointerInputs.kt
+++ b/ui/ui-test/src/androidTest/java/androidx/ui/test/util/PointerInputs.kt
@@ -27,6 +27,7 @@
 import androidx.ui.unit.Duration
 import androidx.ui.unit.IntPxSize
 import androidx.ui.unit.PxPosition
+import androidx.ui.unit.Uptime
 import com.google.common.truth.Truth.assertThat
 
 class PointerInputRecorder : PointerInputModifier {
@@ -67,6 +68,8 @@
         }
 }
 
+fun Uptime.inMilliseconds(): Long = nanoseconds / 1_000_000
+
 val PointerInputRecorder.downEvents get() = events.filter { it.down }
 
 val PointerInputRecorder.recordedDuration: Duration
diff --git a/ui/ui-test/src/main/java/androidx/ui/test/Actions.kt b/ui/ui-test/src/main/java/androidx/ui/test/Actions.kt
index 9a5e8ab..e71f275 100644
--- a/ui/ui-test/src/main/java/androidx/ui/test/Actions.kt
+++ b/ui/ui-test/src/main/java/androidx/ui/test/Actions.kt
@@ -84,10 +84,12 @@
  * Executes the gestures specified in the given block.
  *
  * Example usage:
+ * ```
  * findByTag("myWidget")
  *    .doGesture {
  *        sendSwipeUp()
  *    }
+ * ```
  */
 fun SemanticsNodeInteraction.doGesture(
     block: GestureScope.() -> Unit
@@ -98,6 +100,35 @@
 }
 
 /**
+ * Executes the (partial) gesture specified in the given block. The gesture doesn't need to be
+ * complete and can be resumed later. It is the responsibility of the caller to make sure partial
+ * gestures don't leave the test in an inconsistent state.
+ *
+ * When [sending the down event][sendDown], a token is returned which needs to be used in all
+ * subsequent events of this gesture.
+ *
+ * Example usage:
+ * ```
+ * lateinit var token: GestureToken
+ * findByTag("myWidget")
+ *    .doPartialGesture {
+ *        token = sendDown()
+ *    }
+ *    .assertHasClickAction()
+ *    .doPartialGesture {
+ *        sendUp(token)
+ *    }
+ * ```
+ */
+fun SemanticsNodeInteraction.doPartialGesture(
+    block: PartialGestureScope.() -> Unit
+): SemanticsNodeInteraction {
+    val scope = PartialGestureScope(this)
+    scope.block()
+    return this
+}
+
+/**
  * Provides support to call custom semantics actions on this node.
  *
  * This method is supposed to be used for actions with parameters.
diff --git a/ui/ui-test/src/main/java/androidx/ui/test/GestureScope.kt b/ui/ui-test/src/main/java/androidx/ui/test/GestureScope.kt
index a8cf4ac..0a4675b 100644
--- a/ui/ui-test/src/main/java/androidx/ui/test/GestureScope.kt
+++ b/ui/ui-test/src/main/java/androidx/ui/test/GestureScope.kt
@@ -17,7 +17,6 @@
 package androidx.ui.test
 
 import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
 import androidx.ui.core.gesture.LongPressTimeout
 import androidx.ui.unit.Duration
 import androidx.ui.unit.IntPxSize
@@ -34,26 +33,6 @@
 import kotlin.math.sin
 
 /**
- * An object that has an associated component in which one can inject gestures. The gestures can
- * be injected by calling methods defined on [GestureScope], such as [sendSwipeUp]. The associated
- * component is the [SemanticsNodeInteraction] found by one of the finder methods such as [findByTag].
- *
- * Example usage:
- * findByTag("myWidget")
- *    .doGesture {
- *        sendSwipeUp()
- *    }
- */
-class GestureScope internal constructor(
-    internal val semanticsNodeInteraction: SemanticsNodeInteraction
-) {
-    // TODO(b/133217292): Better error: explain which gesture couldn't be performed
-    // TODO: Avoid calling this multiple times as it involves synchronization.
-    internal inline val semanticsNode
-        get() = semanticsNodeInteraction.fetchSemanticsNode("Failed to perform a gesture.")
-}
-
-/**
  * The distance of a swipe's start position from the node's edge, in terms of the node's length.
  * We do not start the swipe exactly on the node's edge, but somewhat more inward, since swiping
  * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).
@@ -66,17 +45,26 @@
  */
 private val doubleClickDelay = 145.milliseconds
 
+sealed class BaseGestureScope(
+    internal val semanticsNodeInteraction: SemanticsNodeInteraction
+) {
+    // TODO(b/133217292): Better error: explain which gesture couldn't be performed
+    // TODO: Avoid calling this multiple times as it involves synchronization.
+    internal inline val semanticsNode
+        get() = semanticsNodeInteraction.fetchSemanticsNode("Failed to perform a gesture.")
+}
+
 /**
  * Returns the size of the component we're interacting with
  */
-val GestureScope.size: IntPxSize
+val BaseGestureScope.size: IntPxSize
     get() = semanticsNode.size
 
 /**
  * Returns the center of the component we're interacting with, in the component's local
  * coordinate system, where (0.px, 0.px) is the top left corner of the component.
  */
-val GestureScope.center: PxPosition
+val BaseGestureScope.center: PxPosition
     get() {
         return PxPosition(size.width / 2, size.height / 2)
     }
@@ -84,7 +72,7 @@
 /**
  * Returns the global bounds of the component we're interacting with
  */
-val GestureScope.globalBounds: PxBounds
+val BaseGestureScope.globalBounds: PxBounds
     get() = semanticsNode.globalBounds
 
 /**
@@ -92,13 +80,31 @@
  *
  * @param position A position in local coordinates
  */
-fun GestureScope.localToGlobal(position: PxPosition): PxPosition {
+fun BaseGestureScope.localToGlobal(position: PxPosition): PxPosition {
     val bounds = globalBounds
     return position + PxPosition(bounds.left, bounds.top)
 }
 
 /**
- * Performs a click gesture on the given [position] on the associated component. The [position]
+ * The receiver scope for injecting gestures on the node identified by the
+ * [semanticsNodeInteraction]. Gestures can be injected by calling methods defined on
+ * [GestureScope], such as [sendSwipeUp]. The [semanticsNodeInteraction] can be found by one of
+ * the finder methods such as [findByTag].
+ *
+ * Example usage:
+ * ```
+ * findByTag("myWidget")
+ *    .doGesture {
+ *        sendSwipeUp()
+ *    }
+ * ```
+ */
+class GestureScope internal constructor(
+    semanticsNodeInteraction: SemanticsNodeInteraction
+) : BaseGestureScope(semanticsNodeInteraction)
+
+/**
+ * Performs a click gesture at the given [position] on the associated component. The [position]
  * is in the component's local coordinate system, where (0.px, 0.px) is the top left corner of
  * the component.
  *
@@ -113,24 +119,6 @@
 }
 
 /**
- * Dispatches a down event on the given [position] on the associated component. The [position]
- * is in the component's local coordinate system, where (0.px, 0.px) is the top left corner of
- * the component.
- *
- * Throws [AssertionError] when the component doesn't have a bounding rectangle set
- *
- * @param position The position of the down event, in the component's local coordinate system
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun GestureScope.sendTouchDown(position: PxPosition) {
-    semanticsNodeInteraction.sendInput {
-        it.sendDown(localToGlobal(position))
-    }
-}
-
-/**
  * Performs a click gesture on the associated component. The click is done in the middle of the
  * component's bounds.
  *
@@ -141,7 +129,7 @@
 }
 
 /**
- * Performs a long click gesture on the given [position] on the associated component. There will
+ * Performs a long click gesture at the given [position] on the associated component. There will
  * be [LongPressTimeout] + 100 milliseconds time between the down and the up event. The
  * [position] is in the component's local coordinate system, where (0.px, 0.px) is the top left
  * corner of the component.
@@ -156,7 +144,7 @@
 }
 
 /**
- * Performs a long click gesture on the middle of the associated component. There will
+ * Performs a long click gesture at the middle of the associated component. There will
  * be [LongPressTimeout] + 100 milliseconds time between the down and the up event.
  *
  * Throws [AssertionError] when the component doesn't have a bounding rectangle set
@@ -166,7 +154,7 @@
 }
 
 /**
- * Performs a double click gesture on the given [position] on the associated component. The
+ * Performs a double click gesture at the given [position] on the associated component. The
  * [position] is in the component's local coordinate system, where (0.px, 0.px) is the top left
  * corner of the component.
  *
@@ -401,7 +389,29 @@
 }
 
 /**
- * A token to be shared between individual motion events that form a single gesture.
+ * The receiver scope for injecting partial gestures on the node identified by the
+ * [semanticsNodeInteraction]. Gestures can be injected by calling methods defined on
+ * [PartialGestureScope], such as [sendDown]. The [semanticsNodeInteraction] can be found by one
+ * of the finder methods such as [findByTag].
+ *
+ * Example usage:
+ * ```
+ * val position = PxPosition(10.px, 10.px)
+ * lateinit var token: GestureToken
+ * findByTag("myWidget")
+ *    .doPartialGesture { token = sendDown(position) }
+ *    .assertIsDisplayed()
+ *    .doPartialGesture { sendUp(token, position) }
+ * ```
+ */
+class PartialGestureScope internal constructor(
+    semanticsNodeInteraction: SemanticsNodeInteraction
+) : BaseGestureScope(semanticsNodeInteraction)
+
+/**
+ * A token to be shared between individual motion events that form a single gesture. It is
+ * generated by the [sendDown] partial gesture, and must be passed to all subsequent events of
+ * the gesture, such as [sendMoveTo] and [sendUp].
  */
 class GestureToken internal constructor(
     internal val downTime: Long,
@@ -410,3 +420,86 @@
     internal var eventTime: Long = downTime
     internal var finished: Boolean = false
 }
+
+/**
+ * Sends a down event at the given [position] on the associated component. The [position] is in
+ * the component's local coordinate system, where (0.px, 0.px) is the top left corner of the
+ * component. The returned token needs to be used in all subsequent events of this gesture.
+ *
+ * @param position The position of the down event, in the component's local coordinate system
+ * @return A token that identifies this gesture and must be passed to all subsequent events that
+ * are part of this gesture.
+ */
+fun PartialGestureScope.sendDown(position: PxPosition): GestureToken {
+    val globalPosition = localToGlobal(position)
+    lateinit var token: GestureToken
+    semanticsNodeInteraction.sendInput {
+        token = it.sendDown(globalPosition)
+    }
+    return token
+}
+
+/**
+ * Sends a move event at the given [position] on the associated component. The [position] is in
+ * the component's local coordinate system, where (0.px, 0.px) is the top left corner of the
+ * component.
+ *
+ * @param token The token returned from the corresponding [down event][sendDown] that started
+ * this gesture.
+ * @param position The position of the move event, in the component's local coordinate system
+ */
+fun PartialGestureScope.sendMoveTo(token: GestureToken, position: PxPosition) {
+    val globalPosition = localToGlobal(position)
+    semanticsNodeInteraction.sendInput {
+        it.sendMove(token, globalPosition)
+    }
+}
+
+/**
+ * Sends a move event on the associated component, using the last used coordinate and moving it
+ * by the given [delta].
+ *
+ * @param token The token returned from the corresponding [down event][sendDown] that started
+ * this gesture.
+ * @param delta The position for this move event, relative to the last sent event. For example,
+ * `delta = PxPosition(10.px, -10.px) will add 10.px to the last event's x-position, and subtract
+ * 10.px from the last event's y-position.
+ */
+fun PartialGestureScope.sendMoveBy(token: GestureToken, delta: PxPosition) {
+    val globalPosition = token.lastPosition + delta
+    semanticsNodeInteraction.sendInput {
+        it.sendMove(token, globalPosition)
+    }
+}
+
+/**
+ * Sends an up event at the given [position] on the associated component. If [position] is
+ * omitted, the position of the previous event is used. The [position] is in the component's
+ * local coordinate system, where (0.px, 0.px) is the top left corner of the component.
+ *
+ * @param token The token returned from the corresponding [down event][sendDown] that started
+ * this gesture.
+ * @param position The position of the up event, in the component's local coordinate system
+ */
+fun PartialGestureScope.sendUp(token: GestureToken, position: PxPosition? = null) {
+    val globalPosition = position?.let { localToGlobal(it) } ?: token.lastPosition
+    semanticsNodeInteraction.sendInput {
+        it.sendUp(token, globalPosition)
+    }
+}
+
+/**
+ * Sends a cancel event at the given [position] on the associated component. If [position] is
+ * omitted, the position of the previous event is used. The [position] is in the component's
+ * local coordinate system, where (0.px, 0.px) is the top left corner of the component.
+ *
+ * @param token The token returned from the corresponding [down event][sendDown] that started
+ * this gesture.
+ * @param position The position of the cancel event, in the component's local coordinate system
+ */
+fun PartialGestureScope.sendCancel(token: GestureToken, position: PxPosition? = null) {
+    val globalPosition = position?.let { localToGlobal(it) } ?: token.lastPosition
+    semanticsNodeInteraction.sendInput {
+        it.sendCancel(token, globalPosition)
+    }
+}
diff --git a/ui/ui-test/src/main/java/androidx/ui/test/android/AndroidInputDispatcher.kt b/ui/ui-test/src/main/java/androidx/ui/test/android/AndroidInputDispatcher.kt
index 4c81ec9..bb03972 100644
--- a/ui/ui-test/src/main/java/androidx/ui/test/android/AndroidInputDispatcher.kt
+++ b/ui/ui-test/src/main/java/androidx/ui/test/android/AndroidInputDispatcher.kt
@@ -268,13 +268,17 @@
      * immediately without blocking. See also [dispatchInRealTime].
      * @param eventPeriodOverride If set, specifies a different period in milliseconds between
      * two consecutive injected motion events injected by this [AndroidInputDispatcher]. If not
-     * set, the event period of 10 milliseconds is unchanged. See also [eventPeriod].
+     * set, the event period of 10 milliseconds is unchanged.
+     *
+     * @see AndroidInputDispatcher.eventPeriod
      */
     internal class TestRule(
         private val disableDispatchInRealTime: Boolean = false,
         private val eventPeriodOverride: Long? = null
     ) : org.junit.rules.TestRule {
 
+        val eventPeriod get() = AndroidInputDispatcher.eventPeriod
+
         override fun apply(base: Statement, description: Description?): Statement {
             return ModifyingStatement(base)
         }
@@ -285,7 +289,7 @@
                     dispatchInRealTime = false
                 }
                 if (eventPeriodOverride != null) {
-                    eventPeriod = eventPeriodOverride
+                    AndroidInputDispatcher.eventPeriod = eventPeriodOverride
                 }
                 try {
                     base.evaluate()
@@ -294,7 +298,7 @@
                         dispatchInRealTime = true
                     }
                     if (eventPeriodOverride != null) {
-                        eventPeriod = 10L
+                        AndroidInputDispatcher.eventPeriod = 10L
                     }
                 }
             }