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