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