Fix bad historical time when multiple pointers are present
It has been reported that the historical timestamp of events with
multiple pointers is incorrect, as it will always be zero. This
has been confirmed with an additional unit test.
Bug: 302300930
Test: `gradlew :input:input-motionprediction:test`
Change-Id: I2cc80fccd712115984c6bd3598fcfae071d239db
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
index f6841f7d..aca41a1 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
@@ -155,11 +155,13 @@
// Merge single pointer MotionEvent into a single MotionEvent
MotionEvent.PointerCoords[][] pointerCoords =
new MotionEvent.PointerCoords[minHistorySize][pointerCount];
+ long[] pointerEventTimes = new long[minHistorySize];
for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
int historyIndex = 0;
for (BatchedMotionEvent ev :
BatchedMotionEvent.iterate(singlePointerEvents[pointerIndex])) {
pointerCoords[historyIndex][pointerIndex] = ev.coords[0];
+ pointerEventTimes[historyIndex] = ev.timeMs;
if (minHistorySize <= ++historyIndex) {
break;
}
@@ -195,7 +197,10 @@
0 /* source */,
0 /* flags */);
for (int historyIndex = 1; historyIndex < minHistorySize; historyIndex++) {
- multiPointerEvent.addBatch(0, pointerCoords[historyIndex], 0);
+ multiPointerEvent.addBatch(
+ pointerEventTimes[historyIndex],
+ pointerCoords[historyIndex],
+ 0);
}
if (DEBUG_PREDICTION) {
final StringBuilder builder =
diff --git a/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/MotionEventGenerator.kt b/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/MotionEventGenerator.kt
index d7662eb..22b76925 100644
--- a/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/MotionEventGenerator.kt
+++ b/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/MotionEventGenerator.kt
@@ -19,24 +19,45 @@
import android.view.MotionEvent
import androidx.test.core.view.MotionEventBuilder
-class MotionEventGenerator(val xGenerator: (Long) -> Float, val yGenerator: (Long) -> Float) {
+class MotionEventGenerator(
+ val firstXGenerator: (Long) -> Float,
+ val firstYGenerator: (Long) -> Float,
+ val secondXGenerator: ((Long) -> Float)?,
+ val secondYGenerator: ((Long) -> Float)?
+) {
+
+ constructor(
+ firstXGenerator: (Long) -> Float,
+ firstYGenerator: (Long) -> Float
+ ) : this(firstXGenerator, firstYGenerator, null, null)
+
private val downEventTime: Long = 0
private var currentEventTime: Long = downEventTime
- private val startX = 500f
- private val startY = 500f
+ private val firstStartX = 500f
+ private val firstStartY = 500f
+ private val secondStartX = 500f
+ private val secondStartY = 500f
private var sentDown = false
+ private var sentSecondDown = false
fun next(): MotionEvent {
val motionEventBuilder = MotionEventBuilder.newBuilder()
.setEventTime(currentEventTime)
.setDownTime(downEventTime)
- .setActionIndex(0)
- if (sentDown) {
- motionEventBuilder.setAction(MotionEvent.ACTION_MOVE)
- } else {
+ if (!sentDown) {
motionEventBuilder.setAction(MotionEvent.ACTION_DOWN)
+ motionEventBuilder.setActionIndex(0)
sentDown = true
+ if (secondXGenerator == null || secondYGenerator == null) {
+ sentSecondDown = true
+ }
+ } else if (!sentSecondDown) {
+ motionEventBuilder.setAction(MotionEvent.ACTION_POINTER_DOWN)
+ motionEventBuilder.setActionIndex(1)
+ sentSecondDown = true
+ } else {
+ motionEventBuilder.setAction(MotionEvent.ACTION_MOVE)
}
val pointerProperties = MotionEvent.PointerProperties()
@@ -44,13 +65,28 @@
pointerProperties.toolType = MotionEvent.TOOL_TYPE_STYLUS
val coords = MotionEvent.PointerCoords()
- coords.x = startX + xGenerator(currentEventTime - downEventTime)
- coords.y = startY + yGenerator(currentEventTime - downEventTime)
+ coords.x = firstStartX + firstXGenerator(currentEventTime - downEventTime)
+ coords.y = firstStartY + firstYGenerator(currentEventTime - downEventTime)
coords.pressure = 1f
motionEventBuilder.setPointer(pointerProperties, coords)
- currentEventTime += MOTIONEVENT_RATE_MS
+ if (sentDown && secondXGenerator != null && secondYGenerator != null) {
+ val secondPointerProperties = MotionEvent.PointerProperties()
+ secondPointerProperties.id = 1
+ secondPointerProperties.toolType = MotionEvent.TOOL_TYPE_STYLUS
+
+ val secondCoords = MotionEvent.PointerCoords()
+ secondCoords.x = firstStartX + secondXGenerator.invoke(currentEventTime - downEventTime)
+ secondCoords.y = firstStartY + secondYGenerator.invoke(currentEventTime - downEventTime)
+ secondCoords.pressure = 1f
+
+ motionEventBuilder.setPointer(secondPointerProperties, secondCoords)
+ }
+
+ if (sentDown && sentSecondDown) {
+ currentEventTime += MOTIONEVENT_RATE_MS
+ }
return motionEventBuilder.build()
}
diff --git a/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/kalman/MultiPointerPredictorTest.kt b/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/kalman/MultiPointerPredictorTest.kt
new file mode 100644
index 0000000..eacd62f
--- /dev/null
+++ b/input/input-motionprediction/src/test/kotlin/androidx/input/motionprediction/kalman/MultiPointerPredictorTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.input.motionprediction.kalman
+
+import androidx.input.motionprediction.MotionEventGenerator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class MultiPointerPredictorTest {
+
+ // Ensures that the historical time is properly populated (b/302300930)
+ @Test
+ fun historicalTime() {
+ val predictor = MultiPointerPredictor()
+ val generator = MotionEventGenerator(
+ { delta: Long -> delta.toFloat() },
+ { delta: Long -> delta.toFloat() },
+ { delta: Long -> delta.toFloat() },
+ { delta: Long -> delta.toFloat() },
+ )
+ for (i in 1..INITIAL_FEED) {
+ predictor.onTouchEvent(generator.next())
+ }
+ // Get some historical events
+ val predicted = predictor.predict(PREDICT_SAMPLE * generator.getRateMs().toInt())!!
+ assertThat(predicted.getPointerCount()).isEqualTo(2)
+ var historicalTime = predicted.getEventTime()
+ for (i in (PREDICT_SAMPLE - 2) downTo 1) {
+ historicalTime -= generator.getRateMs().toInt();
+ assertThat(predicted.getHistoricalEventTime(i)).isEqualTo(historicalTime)
+ }
+ }
+}
+
+private const val PREDICT_SAMPLE = 5;
+private const val INITIAL_FEED = 20