Use InputEventAssigner to assign input to frame
When a frame is determined to be caused by an input event, it will be
labeled with a specific input event id. The newly added entity,
InputEventAssigner, is responsible for determining which input
event we should use.
For regular ACTION_MOVE events, we will take the latest input event as
the event that has caused the frame. For the initial gesture
(ACTION_DOWN), we will use the first event (the ACTION_DOWN itself) to
figure out the down latency. This will allow us to split up 'down' and
'scroll' latencies.
Bug: 169866723
Test: looked at the data printed locally to logcat
Test: atest InputTests
Change-Id: I8513a36960e5d652d07655d1267e758d0c59ced7
diff --git a/core/java/android/view/InputEventAssigner.java b/core/java/android/view/InputEventAssigner.java
new file mode 100644
index 0000000..c159a12
--- /dev/null
+++ b/core/java/android/view/InputEventAssigner.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID;
+import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+
+/**
+ * Process input events and assign input event id to a specific frame.
+ *
+ * The assigned input event id is determined by where the current gesture is relative to the vsync.
+ * In the middle of the gesture (we already processed some input events, and already received at
+ * least 1 vsync), the latest InputEvent is assigned to the next frame.
+ * If a gesture just started, then the ACTION_DOWN event will be assigned to the next frame.
+ *
+ * Consider the following sequence:
+ * DOWN -> VSYNC 1 -> MOVE 1 -> MOVE 2 -> VSYNC 2.
+ *
+ * For VSYNC 1, we will assign the "DOWN" input event.
+ * For VSYNC 2, we will assign the "MOVE 2" input event.
+ *
+ * Consider another sequence:
+ * DOWN -> MOVE 1 -> MOVE 2 -> VSYNC 1 -> MOVE 3 -> VSYNC 2.
+ *
+ * For VSYNC 1, we will still assign the "DOWN" input event. That means that "MOVE 1" and "MOVE 2"
+ * events are not attributed to any frame.
+ * For VSYNC 2, the "MOVE 3" input event will be assigned.
+ *
+ * @hide
+ */
+public class InputEventAssigner {
+ private static final String TAG = "InputEventAssigner";
+ private boolean mHasUnprocessedDown = false;
+ private int mEventId = INVALID_INPUT_EVENT_ID;
+
+ /**
+ * Notify InputEventAssigner that the Choreographer callback has been processed. This will reset
+ * the 'down' state to assign the latest input event to the current frame.
+ */
+ public void onChoreographerCallback() {
+ // Mark completion of this frame. Use newest input event from now on.
+ mHasUnprocessedDown = false;
+ }
+
+ /**
+ * Process the provided input event to determine which event id to assign to the current frame.
+ * @param event the input event currently being processed
+ * @return the id of the input event to use for the current frame
+ */
+ public int processEvent(InputEvent event) {
+ if (event instanceof KeyEvent) {
+ // We will not do any special handling for key events
+ return event.getId();
+ }
+
+ if (event instanceof MotionEvent) {
+ MotionEvent motionEvent = (MotionEvent) event;
+ final int action = motionEvent.getActionMasked();
+
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mHasUnprocessedDown = false;
+ }
+ if (motionEvent.isFromSource(SOURCE_TOUCHSCREEN) && action == MotionEvent.ACTION_DOWN) {
+ mHasUnprocessedDown = true;
+ mEventId = event.getId();
+ // This will remain 'true' even if we receive a MOVE event, as long as choreographer
+ // hasn't invoked the 'CALLBACK_INPUT' callback.
+ }
+ // Don't update the event id if we haven't processed DOWN yet.
+ if (!mHasUnprocessedDown) {
+ mEventId = event.getId();
+ }
+ return mEventId;
+ }
+
+ throw new IllegalArgumentException("Received unexpected " + event);
+ }
+}
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index d67439c..6801c27 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -3589,6 +3589,7 @@
msg.append(", deviceId=").append(getDeviceId());
msg.append(", source=0x").append(Integer.toHexString(getSource()));
msg.append(", displayId=").append(getDisplayId());
+ msg.append(", eventId=").append(getId());
}
msg.append(" }");
return msg.toString();
diff --git a/core/java/android/view/ViewFrameInfo.java b/core/java/android/view/ViewFrameInfo.java
index d4aaa61..36bf532 100644
--- a/core/java/android/view/ViewFrameInfo.java
+++ b/core/java/android/view/ViewFrameInfo.java
@@ -17,6 +17,7 @@
package android.view;
import android.graphics.FrameInfo;
+import android.os.IInputConstants;
/**
* The timing information of events taking place in ViewRootImpl
@@ -24,32 +25,14 @@
*/
public class ViewFrameInfo {
public long drawStart;
- public long oldestInputEventTime; // the time of the oldest input event consumed for this frame
- public long newestInputEventTime; // the time of the newest input event consumed for this frame
+
+
// Various flags set to provide extra metadata about the current frame. See flag definitions
// inside FrameInfo.
// @see android.graphics.FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED
public long flags;
- /**
- * Update the oldest event time.
- * @param eventTime the time of the input event
- */
- public void updateOldestInputEvent(long eventTime) {
- if (oldestInputEventTime == 0 || eventTime < oldestInputEventTime) {
- oldestInputEventTime = eventTime;
- }
- }
-
- /**
- * Update the newest event time.
- * @param eventTime the time of the input event
- */
- public void updateNewestInputEvent(long eventTime) {
- if (newestInputEventTime == 0 || eventTime > newestInputEventTime) {
- newestInputEventTime = eventTime;
- }
- }
+ private int mInputEventId;
/**
* Populate the missing fields using the data from ViewFrameInfo
@@ -58,8 +41,7 @@
public void populateFrameInfo(FrameInfo frameInfo) {
frameInfo.frameInfo[FrameInfo.FLAGS] |= flags;
frameInfo.frameInfo[FrameInfo.DRAW_START] = drawStart;
- // TODO(b/169866723): Use InputEventAssigner
- frameInfo.frameInfo[FrameInfo.INPUT_EVENT_ID] = newestInputEventTime;
+ frameInfo.frameInfo[FrameInfo.INPUT_EVENT_ID] = mInputEventId;
}
/**
@@ -67,8 +49,7 @@
*/
public void reset() {
drawStart = 0;
- oldestInputEventTime = 0;
- newestInputEventTime = 0;
+ mInputEventId = IInputConstants.INVALID_INPUT_EVENT_ID;
flags = 0;
}
@@ -78,4 +59,12 @@
public void markDrawStart() {
drawStart = System.nanoTime();
}
+
+ /**
+ * Assign the value for input event id
+ * @param eventId the id of the input event
+ */
+ public void setInputEvent(int eventId) {
+ mInputEventId = eventId;
+ }
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index f8e65bd..390e3ae 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -457,6 +457,7 @@
FallbackEventHandler mFallbackEventHandler;
final Choreographer mChoreographer;
protected final ViewFrameInfo mViewFrameInfo = new ViewFrameInfo();
+ private final InputEventAssigner mInputEventAssigner = new InputEventAssigner();
/**
* Update the Choreographer's FrameInfo object with the timing information for the current
@@ -8352,16 +8353,7 @@
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
- long eventTime = q.mEvent.getEventTimeNano();
- long oldestEventTime = eventTime;
- if (q.mEvent instanceof MotionEvent) {
- MotionEvent me = (MotionEvent)q.mEvent;
- if (me.getHistorySize() > 0) {
- oldestEventTime = me.getHistoricalEventTimeNano(0);
- }
- }
- mViewFrameInfo.updateOldestInputEvent(oldestEventTime);
- mViewFrameInfo.updateNewestInputEvent(eventTime);
+ mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent));
deliverInputEvent(q);
}
@@ -8497,6 +8489,11 @@
consumedBatches = false;
}
doProcessInputEvents();
+ if (consumedBatches) {
+ // Must be done after we processed the input events, to mark the completion of the frame
+ // from the input point of view
+ mInputEventAssigner.onChoreographerCallback();
+ }
return consumedBatches;
}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 77ceda9..d663c52 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -421,7 +421,6 @@
"libstatspull",
"libstatssocket",
"libpdfium",
- "libbinder_ndk",
],
static_libs: [
"libgif",
diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h
index 912d04c5..e9b2f4a 100644
--- a/libs/hwui/FrameInfo.h
+++ b/libs/hwui/FrameInfo.h
@@ -80,6 +80,10 @@
explicit UiFrameInfoBuilder(int64_t* buffer) : mBuffer(buffer) {
memset(mBuffer, 0, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));
set(FrameInfoIndex::FrameTimelineVsyncId) = INVALID_VSYNC_ID;
+ // The struct is zeroed by memset above. That also sets FrameInfoIndex::InputEventId to
+ // equal android::os::IInputConstants::INVALID_INPUT_EVENT_ID == 0.
+ // Therefore, we can skip setting the value for InputEventId here. If the value for
+ // INVALID_INPUT_EVENT_ID changes, this code would have to be updated, as well.
set(FrameInfoIndex::FrameDeadline) = std::numeric_limits<int64_t>::max();
}
diff --git a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt
new file mode 100644
index 0000000..2e985fb
--- /dev/null
+++ b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.input
+
+import android.view.InputDevice.SOURCE_MOUSE
+import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.InputEventAssigner
+import android.view.KeyEvent
+import android.view.MotionEvent
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * Create a MotionEvent with the provided action, eventTime, and source
+ */
+fun createMotionEvent(action: Int, eventTime: Long, source: Int): MotionEvent {
+ val downTime: Long = 10
+ val x = 1f
+ val y = 2f
+ val pressure = 3f
+ val size = 1f
+ val metaState = 0
+ val xPrecision = 0f
+ val yPrecision = 0f
+ val deviceId = 1
+ val edgeFlags = 0
+ val displayId = 0
+ return MotionEvent.obtain(downTime, eventTime, action, x, y, pressure, size, metaState,
+ xPrecision, yPrecision, deviceId, edgeFlags, source, displayId)
+}
+
+fun createKeyEvent(action: Int, eventTime: Long): KeyEvent {
+ val code = KeyEvent.KEYCODE_A
+ val repeat = 0
+ return KeyEvent(eventTime, eventTime, action, code, repeat)
+}
+
+class InputEventAssignerTest {
+ companion object {
+ private const val TAG = "InputEventAssignerTest"
+ }
+
+ /**
+ * A single MOVE event should be assigned to the next available frame.
+ */
+ @Test
+ fun testTouchGesture() {
+ val assigner = InputEventAssigner()
+ val event = createMotionEvent(MotionEvent.ACTION_MOVE, 10, SOURCE_TOUCHSCREEN)
+ val eventId = assigner.processEvent(event)
+ assertEquals(event.id, eventId)
+ }
+
+ /**
+ * DOWN event should be used until a vsync comes in. After vsync, the latest event should be
+ * produced.
+ */
+ @Test
+ fun testTouchDownWithMove() {
+ val assigner = InputEventAssigner()
+ val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_TOUCHSCREEN)
+ val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_TOUCHSCREEN)
+ val move2 = createMotionEvent(MotionEvent.ACTION_MOVE, 13, SOURCE_TOUCHSCREEN)
+ val move3 = createMotionEvent(MotionEvent.ACTION_MOVE, 14, SOURCE_TOUCHSCREEN)
+ val move4 = createMotionEvent(MotionEvent.ACTION_MOVE, 15, SOURCE_TOUCHSCREEN)
+ var eventId = assigner.processEvent(down)
+ assertEquals(down.id, eventId)
+ eventId = assigner.processEvent(move1)
+ assertEquals(down.id, eventId)
+ eventId = assigner.processEvent(move2)
+ // Even though we already had 2 move events, there was no choreographer callback yet.
+ // Therefore, we should still get the id of the down event
+ assertEquals(down.id, eventId)
+
+ // Now send CALLBACK_INPUT to the assigner. It should provide the latest motion event
+ assigner.onChoreographerCallback()
+ eventId = assigner.processEvent(move3)
+ assertEquals(move3.id, eventId)
+ eventId = assigner.processEvent(move4)
+ assertEquals(move4.id, eventId)
+ }
+
+ /**
+ * Similar to the above test, but with SOURCE_MOUSE. Since we don't have down latency
+ * concept for non-touchscreens, the latest input event will be used.
+ */
+ @Test
+ fun testMouseDownWithMove() {
+ val assigner = InputEventAssigner()
+ val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_MOUSE)
+ val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_MOUSE)
+ var eventId = assigner.processEvent(down)
+ assertEquals(down.id, eventId)
+ eventId = assigner.processEvent(move1)
+ assertEquals(move1.id, eventId)
+ }
+
+ /**
+ * KeyEvents are processed immediately, so the latest event should be returned.
+ */
+ @Test
+ fun testKeyEvent() {
+ val assigner = InputEventAssigner()
+ val down = createKeyEvent(KeyEvent.ACTION_DOWN, 20)
+ var eventId = assigner.processEvent(down)
+ assertEquals(down.id, eventId)
+ val up = createKeyEvent(KeyEvent.ACTION_UP, 21)
+ eventId = assigner.processEvent(up)
+ // DOWN is only sticky for Motions, not for keys
+ assertEquals(up.id, eventId)
+ assigner.onChoreographerCallback()
+ val down2 = createKeyEvent(KeyEvent.ACTION_DOWN, 22)
+ eventId = assigner.processEvent(down2)
+ assertEquals(down2.id, eventId)
+ }
+}
diff --git a/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt b/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt
index c19e5cc..c01d32b 100644
--- a/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt
+++ b/tests/Input/src/com/android/test/input/ViewFrameInfoTest.kt
@@ -17,6 +17,7 @@
package com.android.test.input
import android.graphics.FrameInfo
+import android.os.IInputConstants.INVALID_INPUT_EVENT_ID
import android.os.SystemClock
import android.view.ViewFrameInfo
import com.google.common.truth.Truth.assertThat
@@ -33,8 +34,7 @@
@Before
fun setUp() {
mViewFrameInfo.reset()
- mViewFrameInfo.updateOldestInputEvent(10)
- mViewFrameInfo.updateNewestInputEvent(20)
+ mViewFrameInfo.setInputEvent(139)
mViewFrameInfo.flags = mViewFrameInfo.flags or FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED
mTimeStarted = SystemClock.uptimeNanos()
mViewFrameInfo.markDrawStart()
@@ -43,8 +43,6 @@
@Test
fun testPopulateFields() {
assertThat(mViewFrameInfo.drawStart).isGreaterThan(mTimeStarted)
- assertThat(mViewFrameInfo.oldestInputEventTime).isEqualTo(10)
- assertThat(mViewFrameInfo.newestInputEventTime).isEqualTo(20)
assertThat(mViewFrameInfo.flags).isEqualTo(FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED)
}
@@ -53,8 +51,6 @@
mViewFrameInfo.reset()
// Ensure that the original object is reset correctly
assertThat(mViewFrameInfo.drawStart).isEqualTo(0)
- assertThat(mViewFrameInfo.oldestInputEventTime).isEqualTo(0)
- assertThat(mViewFrameInfo.newestInputEventTime).isEqualTo(0)
assertThat(mViewFrameInfo.flags).isEqualTo(0)
}
@@ -62,12 +58,13 @@
fun testUpdateFrameInfoFromViewFrameInfo() {
val frameInfo = FrameInfo()
// By default, all values should be zero
- // TODO(b/169866723): Use InputEventAssigner and assert INPUT_EVENT_ID
+ assertThat(frameInfo.frameInfo[FrameInfo.INPUT_EVENT_ID]).isEqualTo(INVALID_INPUT_EVENT_ID)
assertThat(frameInfo.frameInfo[FrameInfo.FLAGS]).isEqualTo(0)
assertThat(frameInfo.frameInfo[FrameInfo.DRAW_START]).isEqualTo(0)
// The values inside FrameInfo should match those from ViewFrameInfo after we update them
mViewFrameInfo.populateFrameInfo(frameInfo)
+ assertThat(frameInfo.frameInfo[FrameInfo.INPUT_EVENT_ID]).isEqualTo(139)
assertThat(frameInfo.frameInfo[FrameInfo.FLAGS]).isEqualTo(
FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED)
assertThat(frameInfo.frameInfo[FrameInfo.DRAW_START]).isGreaterThan(mTimeStarted)