1 /* 2 * Copyright 2015 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 android.hardware.input.cts.tests; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotEquals; 21 import static org.junit.Assert.assertTrue; 22 import static org.junit.Assert.fail; 23 24 import android.app.Instrumentation; 25 import android.hardware.input.cts.InputCallback; 26 import android.hardware.input.cts.InputCtsActivity; 27 import android.util.Log; 28 import android.view.InputDevice; 29 import android.view.InputEvent; 30 import android.view.KeyEvent; 31 import android.view.MotionEvent; 32 import android.view.View; 33 34 import androidx.annotation.NonNull; 35 import androidx.test.platform.app.InstrumentationRegistry; 36 import androidx.test.rule.ActivityTestRule; 37 38 import com.android.compatibility.common.util.PollingCheck; 39 import com.android.cts.input.InputJsonParser; 40 41 import org.junit.After; 42 import org.junit.Before; 43 import org.junit.Rule; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.concurrent.BlockingQueue; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.LinkedBlockingQueue; 50 import java.util.concurrent.TimeUnit; 51 52 public abstract class InputTestCase { 53 private static final String TAG = "InputTestCase"; 54 private static final float TOLERANCE = 0.005f; 55 56 private final BlockingQueue<InputEvent> mEvents; 57 protected final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); 58 59 private InputListener mInputListener; 60 private View mDecorView; 61 62 protected InputJsonParser mParser; 63 // Stores the name of the currently running test 64 protected String mCurrentTestCase; 65 private int mRegisterResourceId; // raw resource that contains json for registering a hid device 66 protected int mVid; 67 protected int mPid; 68 69 // State used for motion events 70 private int mLastButtonState; 71 InputTestCase(int registerResourceId)72 InputTestCase(int registerResourceId) { 73 mEvents = new LinkedBlockingQueue<>(); 74 mInputListener = new InputListener(); 75 mRegisterResourceId = registerResourceId; 76 } 77 78 @Rule 79 public ActivityTestRule<InputCtsActivity> mActivityRule = 80 new ActivityTestRule<>(InputCtsActivity.class); 81 82 @Before setUp()83 public void setUp() throws Exception { 84 mActivityRule.getActivity().clearUnhandleKeyCode(); 85 mDecorView = mActivityRule.getActivity().getWindow().getDecorView(); 86 mParser = new InputJsonParser(mInstrumentation.getTargetContext()); 87 mVid = mParser.readVendorId(mRegisterResourceId); 88 mPid = mParser.readProductId(mRegisterResourceId); 89 int deviceId = mParser.readDeviceId(mRegisterResourceId); 90 String registerCommand = mParser.readRegisterCommand(mRegisterResourceId); 91 setUpDevice(deviceId, mParser.readVendorId(mRegisterResourceId), 92 mParser.readProductId(mRegisterResourceId), 93 mParser.readSources(mRegisterResourceId), registerCommand); 94 mEvents.clear(); 95 } 96 97 @After tearDown()98 public void tearDown() throws Exception { 99 tearDownDevice(); 100 } 101 102 // To be implemented by device specific test case. setUpDevice(int id, int vendorId, int productId, int sources, String registerCommand)103 protected abstract void setUpDevice(int id, int vendorId, int productId, int sources, 104 String registerCommand); 105 tearDownDevice()106 protected abstract void tearDownDevice(); 107 testInputDeviceEvents(int resourceId)108 protected abstract void testInputDeviceEvents(int resourceId); 109 110 /** 111 * Asserts that the application received a {@link android.view.KeyEvent} with the given 112 * metadata. 113 * 114 * If other KeyEvents are received by the application prior to the expected KeyEvent, or no 115 * KeyEvents are received within a reasonable amount of time, then this will throw an 116 * {@link AssertionError}. 117 * 118 * Only action, source, keyCode and metaState are being compared. 119 */ assertReceivedKeyEvent(@onNull KeyEvent expectedKeyEvent)120 private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) { 121 KeyEvent receivedKeyEvent = waitForKey(); 122 if (receivedKeyEvent == null) { 123 failWithMessage("Did not receive " + expectedKeyEvent); 124 } 125 assertEquals(mCurrentTestCase + " (action)", 126 expectedKeyEvent.getAction(), receivedKeyEvent.getAction()); 127 assertSource(mCurrentTestCase, expectedKeyEvent, receivedKeyEvent); 128 assertEquals(mCurrentTestCase + " (keycode)", 129 expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode()); 130 assertMetaState(mCurrentTestCase, expectedKeyEvent.getMetaState(), 131 receivedKeyEvent.getMetaState()); 132 } 133 assertReceivedMotionEvent(@onNull MotionEvent expectedEvent)134 private void assertReceivedMotionEvent(@NonNull MotionEvent expectedEvent) { 135 MotionEvent event = waitForMotion(); 136 /* 137 If the test fails here, one thing to try is to forcefully add a delay after the device 138 added callback has been received, but before any hid data has been written to the device. 139 We already wait for all of the proper callbacks here and in other places of the stack, but 140 it appears that the device sometimes is still not ready to receive hid data. If any data 141 gets written to the device in that state, it will disappear, 142 and no events will be generated. 143 */ 144 145 if (event == null) { 146 failWithMessage("Did not receive " + expectedEvent); 147 } 148 if (event.getHistorySize() > 0) { 149 failWithMessage("expected each MotionEvent to only have a single entry"); 150 } 151 assertEquals(mCurrentTestCase + " (action)", 152 expectedEvent.getAction(), event.getAction()); 153 assertSource(mCurrentTestCase, expectedEvent, event); 154 assertEquals(mCurrentTestCase + " (button state)", 155 expectedEvent.getButtonState(), event.getButtonState()); 156 if (event.getActionMasked() == MotionEvent.ACTION_BUTTON_PRESS 157 || event.getActionMasked() == MotionEvent.ACTION_BUTTON_RELEASE) { 158 // Only checking getActionButton() for ACTION_BUTTON_PRESS or ACTION_BUTTON_RELEASE 159 // because for actions other than ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE the 160 // returned value of getActionButton() is undefined. 161 assertEquals(mCurrentTestCase + " (action button)", 162 mLastButtonState ^ event.getButtonState(), event.getActionButton()); 163 mLastButtonState = event.getButtonState(); 164 } 165 assertAxis(mCurrentTestCase, expectedEvent, event); 166 } 167 168 /** 169 * Asserts motion event axis values. Separate this into a different method to allow individual 170 * test case to specify it. 171 * 172 * @param expectedSource expected source flag specified in JSON files. 173 * @param actualSource actual source flag received in the test app. 174 */ assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent)175 void assertAxis(String testCase, MotionEvent expectedEvent, MotionEvent actualEvent) { 176 for (int i = 0; i < actualEvent.getPointerCount(); i++) { 177 for (int axis = MotionEvent.AXIS_X; axis <= MotionEvent.AXIS_GENERIC_16; axis++) { 178 assertEquals(testCase + " pointer " + i 179 + " (" + MotionEvent.axisToString(axis) + ")", 180 expectedEvent.getAxisValue(axis, i), actualEvent.getAxisValue(axis, i), 181 TOLERANCE); 182 } 183 } 184 } 185 186 /** 187 * Asserts source flags. Separate this into a different method to allow individual test case to 188 * specify it. 189 * The input source check verifies if actual source is equal or a subset of the expected source. 190 * With Linux kernel 4.18 or later the input hid driver could register multiple evdev devices 191 * when the HID descriptor has HID usages for different applications. Android frameworks will 192 * create multiple KeyboardInputMappers for each of the evdev device, and each 193 * KeyboardInputMapper will generate key events with source of the evdev device it belongs to. 194 * As long as the source of these key events is a subset of expected source, we consider it as 195 * a valid source. 196 * 197 * @param expected expected event with source flag specified in JSON files. 198 * @param actual actual event with source flag received in the test app. 199 */ assertSource(String testCase, InputEvent expected, InputEvent actual)200 private void assertSource(String testCase, InputEvent expected, InputEvent actual) { 201 assertNotEquals(testCase + " (source)", InputDevice.SOURCE_CLASS_NONE, actual.getSource()); 202 assertTrue(testCase + " (source)", expected.isFromSource(actual.getSource())); 203 } 204 205 /** 206 * Asserts meta states. Separate this into a different method to allow individual test case to 207 * specify it. 208 * 209 * @param expectedMetaState expected meta state specified in JSON files. 210 * @param actualMetaState actual meta state received in the test app. 211 */ assertMetaState(String testCase, int expectedMetaState, int actualMetaState)212 void assertMetaState(String testCase, int expectedMetaState, int actualMetaState) { 213 assertEquals(testCase + " (meta state)", expectedMetaState, actualMetaState); 214 } 215 216 /** 217 * Assert that no more events have been received by the application. 218 * 219 * If any more events have been received by the application, this will cause failure. 220 */ assertNoMoreEvents()221 private void assertNoMoreEvents() { 222 mInstrumentation.waitForIdleSync(); 223 InputEvent event = mEvents.poll(); 224 if (event == null) { 225 return; 226 } 227 failWithMessage("extraneous events generated: " + event); 228 } 229 verifyEvents(List<InputEvent> events)230 protected void verifyEvents(List<InputEvent> events) { 231 mActivityRule.getActivity().setInputCallback(mInputListener); 232 // Make sure we received the expected input events 233 if (events.size() == 0) { 234 // If no event is expected we need to wait for event until timeout and fail on 235 // any unexpected event received caused by the HID report injection. 236 InputEvent event = waitForEvent(); 237 if (event != null) { 238 fail(mCurrentTestCase + " : Received unexpected event " + event); 239 } 240 return; 241 } 242 for (int i = 0; i < events.size(); i++) { 243 final InputEvent event = events.get(i); 244 try { 245 if (event instanceof MotionEvent) { 246 assertReceivedMotionEvent((MotionEvent) event); 247 continue; 248 } 249 if (event instanceof KeyEvent) { 250 assertReceivedKeyEvent((KeyEvent) event); 251 continue; 252 } 253 } catch (AssertionError error) { 254 throw new AssertionError("Assertion on entry " + i + " failed.", error); 255 } 256 fail("Entry " + i + " is neither a KeyEvent nor a MotionEvent: " + event); 257 } 258 assertNoMoreEvents(); 259 } 260 testInputEvents(int resourceId)261 protected void testInputEvents(int resourceId) { 262 testInputDeviceEvents(resourceId); 263 assertNoMoreEvents(); 264 } 265 waitForEvent()266 private InputEvent waitForEvent() { 267 try { 268 return mEvents.poll(1, TimeUnit.SECONDS); 269 } catch (InterruptedException e) { 270 failWithMessage("unexpectedly interrupted while waiting for InputEvent"); 271 return null; 272 } 273 } 274 275 // Ignore Motion event received during the 5 seconds timeout period. Return on the first Key 276 // event received. waitForKey()277 private KeyEvent waitForKey() { 278 for (int i = 0; i < 5; i++) { 279 InputEvent event = waitForEvent(); 280 if (event instanceof KeyEvent) { 281 return (KeyEvent) event; 282 } 283 } 284 return null; 285 } 286 287 // Ignore Key event received during the 5 seconds timeout period. Return on the first Motion 288 // event received. waitForMotion()289 private MotionEvent waitForMotion() { 290 for (int i = 0; i < 5; i++) { 291 InputEvent event = waitForEvent(); 292 if (event instanceof MotionEvent) { 293 return (MotionEvent) event; 294 } 295 } 296 return null; 297 } 298 299 /** 300 * Since MotionEvents are batched together based on overall system timings (i.e. vsync), we 301 * can't rely on them always showing up batched in the same way. In order to make sure our 302 * test results are consistent, we instead split up the batches so they end up in a 303 * consistent and reproducible stream. 304 * 305 * Note, however, that this ignores the problem of resampling, as we still don't know how to 306 * distinguish resampled events from real events. Only the latter will be consistent and 307 * reproducible. 308 * 309 * @param event The (potentially) batched MotionEvent 310 * @return List of MotionEvents, with each event guaranteed to have zero history size, and 311 * should otherwise be equivalent to the original batch MotionEvent. 312 */ splitBatchedMotionEvent(MotionEvent event)313 private static List<MotionEvent> splitBatchedMotionEvent(MotionEvent event) { 314 List<MotionEvent> events = new ArrayList<>(); 315 final int historySize = event.getHistorySize(); 316 final int pointerCount = event.getPointerCount(); 317 MotionEvent.PointerProperties[] properties = 318 new MotionEvent.PointerProperties[pointerCount]; 319 MotionEvent.PointerCoords[] currentCoords = new MotionEvent.PointerCoords[pointerCount]; 320 for (int p = 0; p < pointerCount; p++) { 321 properties[p] = new MotionEvent.PointerProperties(); 322 event.getPointerProperties(p, properties[p]); 323 currentCoords[p] = new MotionEvent.PointerCoords(); 324 event.getPointerCoords(p, currentCoords[p]); 325 } 326 for (int h = 0; h < historySize; h++) { 327 long eventTime = event.getHistoricalEventTime(h); 328 MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount]; 329 330 for (int p = 0; p < pointerCount; p++) { 331 coords[p] = new MotionEvent.PointerCoords(); 332 event.getHistoricalPointerCoords(p, h, coords[p]); 333 } 334 MotionEvent singleEvent = 335 MotionEvent.obtain(event.getDownTime(), eventTime, event.getAction(), 336 pointerCount, properties, coords, 337 event.getMetaState(), event.getButtonState(), 338 event.getXPrecision(), event.getYPrecision(), 339 event.getDeviceId(), event.getEdgeFlags(), 340 event.getSource(), event.getFlags()); 341 singleEvent.setActionButton(event.getActionButton()); 342 events.add(singleEvent); 343 } 344 345 MotionEvent singleEvent = 346 MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(), 347 pointerCount, properties, currentCoords, 348 event.getMetaState(), event.getButtonState(), 349 event.getXPrecision(), event.getYPrecision(), 350 event.getDeviceId(), event.getEdgeFlags(), 351 event.getSource(), event.getFlags()); 352 singleEvent.setActionButton(event.getActionButton()); 353 events.add(singleEvent); 354 return events; 355 } 356 357 /** 358 * Append the name of the currently executing test case to the fail message. 359 * Dump out the events queue to help debug. 360 */ failWithMessage(String message)361 private void failWithMessage(String message) { 362 if (mEvents.isEmpty()) { 363 Log.i(TAG, "The events queue is empty"); 364 } else { 365 Log.e(TAG, "There are additional events received by the test activity:"); 366 for (InputEvent event : mEvents) { 367 Log.i(TAG, event.toString()); 368 } 369 } 370 fail(mCurrentTestCase + ": " + message); 371 } 372 373 private class InputListener implements InputCallback { 374 @Override onKeyEvent(KeyEvent ev)375 public void onKeyEvent(KeyEvent ev) { 376 try { 377 mEvents.put(new KeyEvent(ev)); 378 } catch (InterruptedException ex) { 379 failWithMessage("interrupted while adding a KeyEvent to the queue"); 380 } 381 } 382 383 @Override onMotionEvent(MotionEvent ev)384 public void onMotionEvent(MotionEvent ev) { 385 try { 386 for (MotionEvent event : splitBatchedMotionEvent(ev)) { 387 mEvents.put(event); 388 } 389 } catch (InterruptedException ex) { 390 failWithMessage("interrupted while adding a MotionEvent to the queue"); 391 } 392 } 393 } 394 requestFocusSync()395 protected void requestFocusSync() { 396 mActivityRule.getActivity().runOnUiThread(() -> { 397 mDecorView.setFocusable(true); 398 mDecorView.setFocusableInTouchMode(true); 399 mDecorView.requestFocus(); 400 }); 401 PollingCheck.waitFor(mDecorView::hasFocus); 402 } 403 404 protected class PointerCaptureSession implements AutoCloseable { PointerCaptureSession()405 protected PointerCaptureSession() { 406 requestFocusSync(); 407 ensurePointerCaptureState(true); 408 } 409 410 @Override close()411 public void close() { 412 ensurePointerCaptureState(false); 413 } 414 ensurePointerCaptureState(boolean enable)415 private void ensurePointerCaptureState(boolean enable) { 416 final CountDownLatch latch = new CountDownLatch(1); 417 mActivityRule.getActivity().setPointerCaptureCallback(hasCapture -> { 418 if (enable == hasCapture) { 419 latch.countDown(); 420 } 421 }); 422 mActivityRule.getActivity().runOnUiThread(enable ? mDecorView::requestPointerCapture 423 : mDecorView::releasePointerCapture); 424 try { 425 if (!latch.await(60, TimeUnit.SECONDS)) { 426 throw new IllegalStateException( 427 "Did not receive callback after " 428 + (enable ? "enabling" : "disabling") 429 + " Pointer Capture."); 430 } 431 } catch (InterruptedException e) { 432 throw new IllegalStateException( 433 "Interrupted while waiting for Pointer Capture state."); 434 } finally { 435 mActivityRule.getActivity().setPointerCaptureCallback(null); 436 } 437 assertEquals("The view's Pointer Capture state did not match.", enable, 438 mDecorView.hasPointerCapture()); 439 } 440 } 441 } 442