blob: 20f7283ba4fffc8cfc7fbb57056fe2235b09250b [file] [log] [blame]
/*
* Copyright (C) 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 android.view.cts;
import static android.view.InputDevice.SOURCE_CLASS_JOYSTICK;
import static android.view.InputDevice.SOURCE_CLASS_NONE;
import static android.view.InputDevice.SOURCE_CLASS_POINTER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.graphics.Point;
import android.os.SystemClock;
import android.view.Choreographer;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.PollingCheck;
import com.android.compatibility.common.util.WindowUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
@MediumTest
@RunWith(AndroidJUnit4.class)
// Test View.requestUnbufferedDispatch API.
//
// In this test, we try to synchronously inject input events to the app, and look at how many events
// we are able to inject during a single VSYNC period.
//
// During normal (buffered) dispatch, input event would remain pending until the VSYNC callback.
// In that scenario, we can send at most 1 input event per VSYNC.
//
// During unbuffered dispatch, input event would be received and handled by the app right away.
// We should be able to send multiple input events to the app within a single VSYNC period.
//
// For this test, we count the max number of synchronously injected events in a single VSYNC period.
// If we can achieve at most 1 event per VSYNC, we must be in normal (batched) mode.
// If we are receiving more than 1 event per VSYNC, we must be in unbuffered mode.
// This test assumes that the time that it takes to inject an event to receiver is less than one
// frame.
//
// This test is executed for joystick, touch, and hover events.
public class ViewUnbufferedTest {
private static final int SAMPLE_COUNT = 20;
private static final int POS_STEP = 5;
@Rule
public ActivityTestRule<ViewUnbufferedTestActivity> mActivityRule =
new ActivityTestRule<>(ViewUnbufferedTestActivity.class);
private Instrumentation mInstrumentation;
private UiAutomation mAutomation;
private Activity mActivity;
private View mView;
private Choreographer mChoreographer;
private final BlockingQueue<ReceivedEvent> mReceivedEvents = new LinkedBlockingQueue<>();
private final BlockingQueue<MotionEvent> mSentEvents = new LinkedBlockingQueue<>();
private AtomicInteger mReceivedCountPerFrame = new AtomicInteger(0);
private int mMaxReceivedCountPerFrame;
private boolean mIsRunning;
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long l) {
mReceivedCountPerFrame.set(0);
if (mIsRunning) {
mChoreographer.postFrameCallback(this);
}
}
};
private final class ReceivedEvent {
final long mEventTime;
final int mAction;
final int mX;
final int mY;
final int mSource;
ReceivedEvent(long eventTime, int action, int x, int y, int source) {
mEventTime = eventTime;
mAction = action;
mX = x;
mY = y;
mSource = source;
}
}
@Before
public void setUp() throws Exception {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mAutomation = mInstrumentation.getUiAutomation();
mActivity = mActivityRule.getActivity();
mInstrumentation.runOnMainSync(() -> mChoreographer = Choreographer.getInstance());
mReceivedEvents.clear();
mSentEvents.clear();
mReceivedCountPerFrame.set(0);
mMaxReceivedCountPerFrame = 0;
WindowUtil.waitForFocus(mActivity);
mView = mActivity.findViewById(R.id.test_view);
// Make sure all window animations are completed.
mAutomation.syncInputTransactions(true /* waitForAnimations */);
}
@After
public void tearDown() {
mIsRunning = false;
mChoreographer.removeFrameCallback(mFrameCallback);
}
// If resampling happened, the coordinates and event time would resample to new position.
private static void compareEvent(final MotionEvent sentEvent,
final ReceivedEvent receivedEvent, final int[] offsets) {
assertEquals(sentEvent.getAction(), receivedEvent.mAction);
assertEquals((int) sentEvent.getX(), receivedEvent.mX + offsets[0]);
assertEquals((int) sentEvent.getY(), receivedEvent.mY + offsets[1]);
assertEquals(sentEvent.getEventTime(), receivedEvent.mEventTime);
assertEquals(sentEvent.getSource(), receivedEvent.mSource);
}
private void compareEvents(final BlockingQueue<MotionEvent> sentEvents,
final BlockingQueue<ReceivedEvent> receivedEvents, final int[] offsets) {
assertEquals(sentEvents.size(), receivedEvents.size());
for (int i = 0; i < sentEvents.size(); i++) {
MotionEvent sentEvent = sentEvents.poll();
ReceivedEvent receivedEvent = receivedEvents.poll();
compareEvent(sentEvent, receivedEvent, offsets);
}
}
private int[] getViewLocationOnScreen(View view) {
final int[] xy = new int[2];
view.getLocationOnScreen(xy);
return xy;
}
private Point getViewCenterOnScreen(View view) {
final int[] xy = getViewLocationOnScreen(view);
final int viewWidth = view.getWidth();
final int viewHeight = view.getHeight();
return new Point(xy[0] + viewWidth / 2, xy[1] + viewHeight / 2);
}
// Sent events with first and last events, rests are move events.
private void sendPointerEvents(int x, int y, int[] actions, int source) {
// Send a start event to make sure view handled unbuffered request.
final long downTime = SystemClock.uptimeMillis();
MotionEvent downEvent =
MotionEvent.obtain(downTime, downTime, actions[0], x, y, 0);
downEvent.setSource(source);
mAutomation.injectInputEvent(downEvent, true /* sync */);
// Inject move events.
startResetReceivedCountPerFrame();
for (int i = 0; i < SAMPLE_COUNT; i++, x += POS_STEP) {
// Stop the loop when we receive more than 1 event, since we only care about whether
// the events are buffered or not, and not about finding the actual max value.
if (mMaxReceivedCountPerFrame > 1) {
break;
}
final long eventTime = SystemClock.uptimeMillis();
final MotionEvent moveEvent = MotionEvent.obtain(downTime, eventTime,
actions[1], x, y, 0);
moveEvent.setSource(source);
mAutomation.injectInputEvent(moveEvent, true /* sync */);
mSentEvents.add(moveEvent);
}
// Send end event to make sure all events had been handled.
final MotionEvent upEvent = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
actions[2], x, y, 0);
upEvent.setSource(source);
mAutomation.injectInputEvent(upEvent, true /* sync */);
}
// Joystick events could always be move events.
private void sendJoystickEvents(int x, int y) {
// Inject move events.
startResetReceivedCountPerFrame();
for (int i = 0; i < SAMPLE_COUNT; i++, x += POS_STEP) {
// Stop the loop when we receive more than 1 event, since we only care about whether
// the events are buffered or not, and not about finding the actual max value.
if (mMaxReceivedCountPerFrame > 1) {
break;
}
final long eventTime = SystemClock.uptimeMillis();
final MotionEvent moveEvent = MotionEvent.obtain(eventTime, eventTime,
MotionEvent.ACTION_MOVE, x, y, 0);
moveEvent.setSource(InputDevice.SOURCE_JOYSTICK);
mAutomation.injectInputEvent(moveEvent, true /* sync */);
mSentEvents.add(moveEvent);
}
}
// Store move/hover move event into mReceivedEvents and unpack any historical
// events.
private boolean receivedEvent(MotionEvent event) {
// Only care about move or hover move due to rest are not to be batched and might be sent
// in same frame.
if (event.getAction() != MotionEvent.ACTION_MOVE
&& event.getAction() != MotionEvent.ACTION_HOVER_MOVE) {
// Always return true to make sure the event has been handled.
return true;
}
// Increase the count and store the maximum value.
final int count = mReceivedCountPerFrame.incrementAndGet();
if (count > mMaxReceivedCountPerFrame) {
mMaxReceivedCountPerFrame = count;
}
mReceivedEvents.add(new ReceivedEvent(event.getEventTime(), event.getAction(),
Math.round(event.getX()), Math.round(event.getY()), event.getSource()));
// Always return true to make sure the event has been handled.
return true;
}
// Start the loop to reset the received count per frame.
private void startResetReceivedCountPerFrame() {
mIsRunning = true;
mChoreographer.postFrameCallback(mFrameCallback);
}
// Normal view would expect to receive the buffered event.
@Test
public void testNormalTouch() {
mView.setOnTouchListener((v, event) -> receivedEvent(event));
final Point point = getViewCenterOnScreen(mView);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP},
InputDevice.SOURCE_TOUCHSCREEN);
// Expect the buffered event could be received 1 per frame.
assertFalse(mReceivedEvents.isEmpty());
assertEquals(1, mMaxReceivedCountPerFrame);
}
// Test view requested touch screen unbuffered.
@Test
public void testUnbufferedTouch() {
mView.setOnTouchListener((v, event) -> receivedEvent(event));
mView.requestUnbufferedDispatch(SOURCE_CLASS_POINTER);
final Point point = getViewCenterOnScreen(mView);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP},
InputDevice.SOURCE_TOUCHSCREEN);
assertTrue(mMaxReceivedCountPerFrame > 1);
compareEvents(mSentEvents, mReceivedEvents, getViewLocationOnScreen(mView));
}
// Test view requested touch screen unbuffered from MotionEvent.
@Test
public void testUnbufferedTouchByEvent() {
mView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
v.requestUnbufferedDispatch(event);
}
return receivedEvent(event);
});
final Point point = getViewCenterOnScreen(mView);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP},
InputDevice.SOURCE_TOUCHSCREEN);
assertTrue(mMaxReceivedCountPerFrame > 1);
compareEvents(mSentEvents, mReceivedEvents, getViewLocationOnScreen(mView));
}
// Test view requested unbuffered source but reset it later.
@Test
public void testUnbufferedTouch_Reset() {
mView.setOnTouchListener((v, event) -> receivedEvent(event));
mView.requestUnbufferedDispatch(SOURCE_CLASS_POINTER);
final Point point = getViewCenterOnScreen(mView);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP},
InputDevice.SOURCE_TOUCHSCREEN);
assertTrue(mMaxReceivedCountPerFrame > 1);
// Reset the unbuffered dispatch and re-send the events.
mMaxReceivedCountPerFrame = 0;
mView.requestUnbufferedDispatch(SOURCE_CLASS_NONE);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP},
InputDevice.SOURCE_TOUCHSCREEN);
assertEquals(1, mMaxReceivedCountPerFrame);
}
// Test view requested joystick unbuffered.
@Test
public void testUnbufferedJoystick() {
mView.setOnGenericMotionListener((v, event) -> receivedEvent(event));
mView.requestUnbufferedDispatch(SOURCE_CLASS_JOYSTICK);
PollingCheck.waitFor(mView::hasFocus);
sendJoystickEvents(0, 0);
assertTrue(mMaxReceivedCountPerFrame > 1);
compareEvents(mSentEvents, mReceivedEvents, new int[]{0, 0});
}
// Test view requested joystick unbuffered but no focus.
@Test
public void testUnbufferedJoystick_NoFocus() {
mView.setOnGenericMotionListener((v, event) -> receivedEvent(event));
mView.requestUnbufferedDispatch(SOURCE_CLASS_JOYSTICK);
final View normalView = mActivity.findViewById(R.id.normal_view);
normalView.setOnGenericMotionListener((v, event) -> receivedEvent(event));
mInstrumentation.runOnMainSync(() -> normalView.requestFocus());
PollingCheck.waitFor(normalView::hasFocus);
sendJoystickEvents(0, 0);
assertEquals(1, mMaxReceivedCountPerFrame);
}
// Test view requested unbuffered for hover events.
@Test
public void testUnbufferedHover() {
mView.setOnHoverListener((v, event) -> receivedEvent(event));
mView.requestUnbufferedDispatch(SOURCE_CLASS_POINTER);
final Point point = getViewCenterOnScreen(mView);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_HOVER_ENTER,
MotionEvent.ACTION_HOVER_MOVE,
MotionEvent.ACTION_HOVER_EXIT},
InputDevice.SOURCE_TOUCHSCREEN);
assertTrue(mMaxReceivedCountPerFrame > 1);
compareEvents(mSentEvents, mReceivedEvents, getViewLocationOnScreen(mView));
}
// Test view requested different source unbuffered from the received events.
@Test
public void testUnbuffered_DifferentSource() {
mView.setOnTouchListener((v, event) -> receivedEvent(event));
mView.requestUnbufferedDispatch(SOURCE_CLASS_JOYSTICK);
final Point point = getViewCenterOnScreen(mView);
sendPointerEvents(point.x, point.y,
new int[]{MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP},
InputDevice.SOURCE_TOUCHSCREEN);
assertEquals(1, mMaxReceivedCountPerFrame);
}
}