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