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