1 /* 2 * Copyright (C) 2023 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.car.cts; 18 19 import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER; 20 import static android.car.cts.utils.ShellPermissionUtils.runWithShellPermissionIdentity; 21 import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING; 22 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING; 23 import static android.car.media.CarAudioManager.CarVolumeCallback; 24 25 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 26 27 import static com.google.common.truth.Truth.assertThat; 28 import static com.google.common.truth.Truth.assertWithMessage; 29 30 import static org.junit.Assume.assumeNotNull; 31 import static org.junit.Assume.assumeTrue; 32 33 import android.app.Activity; 34 import android.app.ActivityOptions; 35 import android.car.Car; 36 import android.car.CarOccupantZoneManager; 37 import android.car.CarOccupantZoneManager.OccupantZoneInfo; 38 import android.car.annotation.ApiRequirements; 39 import android.car.media.CarAudioManager; 40 import android.car.test.PermissionsCheckerRule.EnsureHasPermission; 41 import android.content.Intent; 42 import android.graphics.Point; 43 import android.graphics.Rect; 44 import android.os.ConditionVariable; 45 import android.util.SparseArray; 46 import android.view.Display; 47 import android.view.InputEvent; 48 import android.view.KeyEvent; 49 import android.view.MotionEvent; 50 51 import androidx.lifecycle.Lifecycle; 52 import androidx.test.core.app.ActivityScenario; 53 import androidx.test.filters.FlakyTest; 54 import androidx.test.runner.AndroidJUnit4; 55 56 import com.android.compatibility.common.util.CddTest; 57 import com.android.compatibility.common.util.PollingCheck; 58 import com.android.internal.annotations.GuardedBy; 59 60 import org.junit.After; 61 import org.junit.Before; 62 import org.junit.Test; 63 import org.junit.runner.RunWith; 64 65 import java.util.List; 66 import java.util.concurrent.CountDownLatch; 67 import java.util.concurrent.LinkedBlockingQueue; 68 import java.util.concurrent.TimeUnit; 69 import java.util.function.BiConsumer; 70 71 @RunWith(AndroidJUnit4.class) 72 @FlakyTest(bugId = 279829443) 73 public class CarInputTest extends AbstractCarTestCase { 74 private static final String TAG = CarInputTest.class.getSimpleName(); 75 private static final long ACTIVITY_WAIT_TIME_OUT_MS = 10_000L; 76 private static final int DEFAULT_WAIT_MS = 5_000; 77 private static final int NO_EVENT_WAIT_MS = 100; 78 private static final String PREFIX_INJECTING_KEY_CMD = "cmd car_service inject-key"; 79 private static final String PREFIX_INJECTING_MOTION_CMD = "cmd car_service inject-motion"; 80 private static final String OPTION_SEAT = " -s "; 81 private static final String OPTION_ACTION = " -a "; 82 private static final String OPTION_COUNT = " -c "; 83 private static final String OPTION_POINTER_ID = " -p "; 84 85 private CarOccupantZoneManager mCarOccupantZoneManager; 86 private SparseArray<ActivityScenario<TestActivity>> mActivityScenariosPerDisplay = 87 new SparseArray<>(); 88 private SparseArray<TestActivity> mActivitiesPerDisplay = new SparseArray<>(); 89 90 @Before setUp()91 public void setUp() throws Exception { 92 mCarOccupantZoneManager = 93 (CarOccupantZoneManager) getCar().getCarManager(Car.CAR_OCCUPANT_ZONE_SERVICE); 94 } 95 96 @After tearDown()97 public void tearDown() throws Exception { 98 clearActivities(); 99 } 100 clearActivities()101 private void clearActivities() { 102 for (int i = 0; i < mActivityScenariosPerDisplay.size(); i++) { 103 mActivityScenariosPerDisplay.valueAt(i).close(); 104 } 105 mActivityScenariosPerDisplay.clear(); 106 mActivitiesPerDisplay.clear(); 107 } 108 109 @Test 110 @CddTest(requirements = {"TODO(b/262236403)"}) 111 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 112 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) testHomeKeyForEachPassengerMainDisplay_bringsHomeForTheDisplayOnly()113 public void testHomeKeyForEachPassengerMainDisplay_bringsHomeForTheDisplayOnly() 114 throws Exception { 115 forEachPassengerMainDisplay((zone, display) -> { 116 launchActivitiesOnAllMainDisplays(); 117 int targetDisplayId = display.getDisplayId(); 118 119 injectKeyByShell(zone, KeyEvent.KEYCODE_HOME); 120 121 PollingCheck.waitFor(DEFAULT_WAIT_MS, () -> { 122 return mActivityScenariosPerDisplay.get(targetDisplayId).getState() 123 != Lifecycle.State.RESUMED; 124 }, "Unable to reach home screen on display " + targetDisplayId); 125 for (int i = 0; i < mActivityScenariosPerDisplay.size(); i++) { 126 int displayId = mActivityScenariosPerDisplay.keyAt(i); 127 if (displayId == targetDisplayId) { 128 continue; 129 } 130 assertWithMessage("Home key should not affect the other displays." 131 + " Home key was injected to display " + targetDisplayId + ", but display " 132 + displayId + " was affected.") 133 .that(mActivityScenariosPerDisplay.valueAt(i).getState()) 134 .isEqualTo(Lifecycle.State.RESUMED); 135 } 136 // Recreate the test activity for next test 137 clearActivities(); 138 }); 139 } 140 141 @Test 142 @CddTest(requirements = {"TODO(b/262236403)"}) 143 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 144 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) testBackKeyForEachPassengerMainDisplay()145 public void testBackKeyForEachPassengerMainDisplay() throws Exception { 146 launchActivitiesOnAllMainDisplays(); 147 forEachPassengerMainDisplay((zone, display) -> { 148 int displayId = display.getDisplayId(); 149 150 injectKeyByShell(zone, KeyEvent.KEYCODE_BACK); 151 152 assertReceivedKeyCode(displayId, KeyEvent.KEYCODE_BACK); 153 }); 154 } 155 156 @Test 157 @CddTest(requirements = {"TODO(b/262236403)"}) 158 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 159 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) testAKeyForEachPassengerMainDisplay()160 public void testAKeyForEachPassengerMainDisplay() throws Exception { 161 launchActivitiesOnAllMainDisplays(); 162 forEachPassengerMainDisplay((zone, display) -> { 163 int displayId = display.getDisplayId(); 164 165 injectKeyByShell(zone, KeyEvent.KEYCODE_A); 166 167 assertReceivedKeyCode(displayId, KeyEvent.KEYCODE_A); 168 }); 169 } 170 171 @Test 172 @CddTest(requirements = {"TODO(b/262236403)"}) 173 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 174 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) testPowerKeyForEachPassengerMainDisplay()175 public void testPowerKeyForEachPassengerMainDisplay() throws Exception { 176 forEachPassengerMainDisplay((zone, display) -> { 177 // Screen off 178 injectKeyByShell(zone, KeyEvent.KEYCODE_POWER); 179 PollingCheck.waitFor(DEFAULT_WAIT_MS, () -> { 180 return display.getState() == Display.STATE_OFF; 181 }, "Display state should be off"); 182 183 // Screen on 184 injectKeyByShell(zone, KeyEvent.KEYCODE_POWER); 185 PollingCheck.waitFor(DEFAULT_WAIT_MS, () -> { 186 return display.getState() == Display.STATE_ON; 187 }, "Display state should be on"); 188 }); 189 } 190 191 @Test 192 @CddTest(requirements = {"TODO(b/262236403)"}) 193 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 194 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) 195 @EnsureHasPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) testVolumeDownKeyForEachPassengerMainDisplay()196 public void testVolumeDownKeyForEachPassengerMainDisplay() throws Exception { 197 CarAudioManager audioManager = (CarAudioManager) getCar().getCarManager(Car.AUDIO_SERVICE); 198 assumeTrue(audioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)); 199 CarVolumeMonitor callback = new CarVolumeMonitor(); 200 audioManager.registerCarVolumeCallback(callback); 201 202 try { 203 forEachPassengerMainDisplay((zone, display) -> { 204 injectKeyByShell(zone, KeyEvent.KEYCODE_VOLUME_DOWN); 205 206 assertWithMessage("CarVolumeCallback#onGroupVolumeChanged should be called") 207 .that(callback.receivedGroupVolumeChanged(zone.zoneId)) 208 .isTrue(); 209 callback.reset(); 210 }); 211 } finally { 212 audioManager.unregisterCarVolumeCallback(callback); 213 } 214 } 215 216 @Test 217 @CddTest(requirements = {"TODO(b/262236403)"}) 218 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 219 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) 220 @EnsureHasPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) testVolumeUpKeyForEachPassengerMainDisplay()221 public void testVolumeUpKeyForEachPassengerMainDisplay() throws Exception { 222 CarAudioManager audioManager = (CarAudioManager) getCar().getCarManager(Car.AUDIO_SERVICE); 223 assumeTrue(audioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)); 224 CarVolumeMonitor callback = new CarVolumeMonitor(); 225 audioManager.registerCarVolumeCallback(callback); 226 227 try { 228 forEachPassengerMainDisplay((zone, display) -> { 229 injectKeyByShell(zone, KeyEvent.KEYCODE_VOLUME_UP); 230 231 assertWithMessage("CarVolumeCallback#onGroupVolumeChanged should be called") 232 .that(callback.receivedGroupVolumeChanged(zone.zoneId)) 233 .isTrue(); 234 callback.reset(); 235 }); 236 } finally { 237 audioManager.unregisterCarVolumeCallback(callback); 238 } 239 } 240 241 @Test 242 @CddTest(requirements = {"TODO(b/262236403)"}) 243 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 244 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) 245 @EnsureHasPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) testVolumeMuteKeyForEachPassengerMainDisplay()246 public void testVolumeMuteKeyForEachPassengerMainDisplay() throws Exception { 247 CarAudioManager audioManager = (CarAudioManager) getCar().getCarManager(Car.AUDIO_SERVICE); 248 assumeTrue(audioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)); 249 assumeTrue(audioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)); 250 CarVolumeMonitor callback = new CarVolumeMonitor(); 251 audioManager.registerCarVolumeCallback(callback); 252 253 try { 254 forEachPassengerMainDisplay((zone, display) -> { 255 try { 256 injectKeyByShell(zone, KeyEvent.KEYCODE_VOLUME_MUTE); 257 258 assertWithMessage("CarVolumeCallback#onMasterMuteChanged should be called") 259 .that(callback.receivedGroupMuteChanged(zone.zoneId)) 260 .isTrue(); 261 assertThat(audioManager.isVolumeGroupMuted(zone.zoneId, callback.groupId)) 262 .isTrue(); 263 } finally { 264 injectKeyByShell(zone, KeyEvent.KEYCODE_VOLUME_MUTE); 265 } 266 }); 267 } finally { 268 audioManager.unregisterCarVolumeCallback(callback); 269 } 270 } 271 272 @Test 273 @CddTest(requirements = {"TODO(b/262236403)"}) 274 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 275 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) testSingleTouchForEachPassengerMainDisplay()276 public void testSingleTouchForEachPassengerMainDisplay() throws Exception { 277 launchActivitiesOnAllMainDisplays(); 278 forEachPassengerMainDisplay((zone, display) -> { 279 int displayId = display.getDisplayId(); 280 Point pointer1 = getDisplayCenter(displayId); 281 282 injectTouchByShell(zone, MotionEvent.ACTION_DOWN, pointer1); 283 assertReceivedMotionAction(displayId, MotionEvent.ACTION_DOWN); 284 285 pointer1.offset(1, 1); 286 injectTouchByShell(zone, MotionEvent.ACTION_MOVE, pointer1); 287 assertReceivedMotionAction(displayId, MotionEvent.ACTION_MOVE); 288 289 injectTouchByShell(zone, MotionEvent.ACTION_UP, pointer1); 290 assertReceivedMotionAction(displayId, MotionEvent.ACTION_UP); 291 }); 292 } 293 294 @Test 295 @CddTest(requirements = {"TODO(b/262236403)"}) 296 @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, 297 minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) testMultiTouchForEachPassengerMainDisplay()298 public void testMultiTouchForEachPassengerMainDisplay() throws Exception { 299 launchActivitiesOnAllMainDisplays(); 300 forEachPassengerMainDisplay((zone, display) -> { 301 int displayId = display.getDisplayId(); 302 Point pointer1 = getDisplayCenter(displayId); 303 Point pointer2 = getDisplayCenter(displayId); 304 pointer2.offset(100, 100); 305 Point[] pointers = new Point[] {pointer1, pointer2}; 306 307 injectTouchByShell(zone, MotionEvent.ACTION_DOWN, pointer1); 308 assertReceivedMotionAction(displayId, MotionEvent.ACTION_DOWN); 309 310 injectTouchByShell(zone, MotionEvent.ACTION_POINTER_DOWN, pointers); 311 assertReceivedMotionAction(displayId, MotionEvent.ACTION_POINTER_DOWN); 312 313 pointer2.offset(1, 1); 314 injectTouchByShell(zone, MotionEvent.ACTION_MOVE, pointers); 315 assertReceivedMotionAction(displayId, MotionEvent.ACTION_MOVE); 316 317 injectTouchByShell(zone, MotionEvent.ACTION_POINTER_UP, pointers); 318 assertReceivedMotionAction(displayId, MotionEvent.ACTION_POINTER_UP); 319 320 injectTouchByShell(zone, MotionEvent.ACTION_UP, pointer1); 321 assertReceivedMotionAction(displayId, MotionEvent.ACTION_UP); 322 }); 323 } 324 assertReceivedKeyCode(int displayId, int keyCode)325 private void assertReceivedKeyCode(int displayId, int keyCode) throws Exception { 326 InputEvent downEvent = mActivitiesPerDisplay.get(displayId).getInputEvent(); 327 InputEvent upEvent = mActivitiesPerDisplay.get(displayId).getInputEvent(); 328 assertWithMessage("Activity on display " + displayId + " must receive key event, keyCode=" 329 + KeyEvent.keyCodeToString(keyCode)) 330 .that(downEvent instanceof KeyEvent).isTrue(); 331 assertWithMessage("Activity on display " + displayId + " must receive key event, keyCode=" 332 + KeyEvent.keyCodeToString(keyCode)) 333 .that(upEvent instanceof KeyEvent).isTrue(); 334 335 KeyEvent downKey = (KeyEvent) downEvent; 336 KeyEvent upKey = (KeyEvent) upEvent; 337 assertWithMessage("Activity on display " + displayId 338 + " must receive " + KeyEvent.keyCodeToString(keyCode)) 339 .that(downKey.getKeyCode()).isEqualTo(keyCode); 340 assertWithMessage("Activity on display " + displayId 341 + " must receive down event, keyCode=" + KeyEvent.keyCodeToString(keyCode)) 342 .that(downKey.getAction()).isEqualTo(KeyEvent.ACTION_DOWN); 343 assertWithMessage("Activity on display " + displayId 344 + " must receive " + KeyEvent.keyCodeToString(keyCode)) 345 .that(upKey.getKeyCode()).isEqualTo(keyCode); 346 assertWithMessage("Activity on display " + displayId 347 + " must receive up event, keyCode=" + KeyEvent.keyCodeToString(keyCode)) 348 .that(upKey.getAction()).isEqualTo(KeyEvent.ACTION_UP); 349 350 assertNoEventsExceptFor(displayId); 351 } 352 assertReceivedMotionAction(int displayId, int actionMasked)353 private void assertReceivedMotionAction(int displayId, int actionMasked) throws Exception { 354 InputEvent event = mActivitiesPerDisplay.get(displayId).getInputEvent(); 355 assertWithMessage("Activity on display " + displayId + " must receive motion event, action=" 356 + MotionEvent.actionToString(actionMasked)) 357 .that(event instanceof MotionEvent).isTrue(); 358 MotionEvent motionEvent = (MotionEvent) event; 359 assertWithMessage("Activity on display " + displayId 360 + " must receive " + MotionEvent.actionToString(actionMasked)) 361 .that(motionEvent.getActionMasked()).isEqualTo(actionMasked); 362 assertNoEventsExceptFor(displayId); 363 } 364 assertNoEventsExceptFor(int displayId)365 private void assertNoEventsExceptFor(int displayId) throws Exception { 366 for (int i = 0; i < mActivitiesPerDisplay.size(); i++) { 367 if (mActivitiesPerDisplay.keyAt(i) == displayId) { 368 continue; 369 } 370 mActivitiesPerDisplay.valueAt(i).assertNoEvents(); 371 } 372 } 373 assertNoEvents(int displayId)374 private void assertNoEvents(int displayId) throws Exception { 375 mActivitiesPerDisplay.get(displayId).assertNoEvents(); 376 } 377 launchActivitiesOnAllMainDisplays()378 private void launchActivitiesOnAllMainDisplays() throws Exception { 379 // Launch the TestActivity on all main displays for driver and passenger 380 forEachMainDisplay(/* includesDriver= */ true, (zone, display) -> { 381 launchActivity(display.getDisplayId()); 382 }); 383 } 384 launchActivity(int displayId)385 private void launchActivity(int displayId) { 386 ConditionVariable activityReferenceObtained = new ConditionVariable(); 387 // Uses ShellPermisson to launch an Activitiy on the different displays. 388 runWithShellPermissionIdentity(() -> { 389 Intent intent = new Intent(mContext, TestActivity.class); 390 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 391 ActivityScenario<TestActivity> activityScenario = ActivityScenario.launch(intent, 392 ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle()); 393 activityScenario.onActivity(activity -> { 394 mActivitiesPerDisplay.put(displayId, activity); 395 activityReferenceObtained.open(); 396 }); 397 mActivityScenariosPerDisplay.put(displayId, activityScenario); 398 }); 399 activityReferenceObtained.block(ACTIVITY_WAIT_TIME_OUT_MS); 400 assertWithMessage("Failed to acquire activity reference.") 401 .that(mActivitiesPerDisplay.get(displayId)) 402 .isNotNull(); 403 } 404 injectKeyByShell(OccupantZoneInfo zone, int keyCode)405 private static void injectKeyByShell(OccupantZoneInfo zone, int keyCode) { 406 assumeNotNull(zone); 407 408 // Generate a command message 409 runShellCommand(PREFIX_INJECTING_KEY_CMD + OPTION_SEAT + zone.seat + ' ' + keyCode); 410 } 411 injectTouchByShell(OccupantZoneInfo zone, int action, Point p)412 private static void injectTouchByShell(OccupantZoneInfo zone, int action, Point p) { 413 injectTouchByShell(zone, action, new Point[] {p}); 414 } 415 injectTouchByShell(OccupantZoneInfo zone, int action, Point[] p)416 private static void injectTouchByShell(OccupantZoneInfo zone, int action, Point[] p) { 417 assumeNotNull(zone); 418 419 int pointerCount = p.length; 420 if (action == MotionEvent.ACTION_POINTER_DOWN || action == MotionEvent.ACTION_POINTER_UP) { 421 int index = p.length - 1; 422 action = (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + action; 423 } 424 425 // Generate a command message 426 StringBuilder sb = new StringBuilder() 427 .append(PREFIX_INJECTING_MOTION_CMD) 428 .append(OPTION_SEAT) 429 .append(zone.seat) 430 .append(OPTION_ACTION) 431 .append(action) 432 .append(OPTION_COUNT) 433 .append(pointerCount); 434 sb.append(OPTION_POINTER_ID); 435 for (int i = 0; i < pointerCount; i++) { 436 sb.append(i); 437 sb.append(' '); 438 } 439 for (int i = 0; i < pointerCount; i++) { 440 sb.append(p[i].x); 441 sb.append(' '); 442 sb.append(p[i].y); 443 sb.append(' '); 444 } 445 runShellCommand(sb.toString()); 446 } 447 getDisplayCenter(int displayId)448 private Point getDisplayCenter(int displayId) { 449 Rect rect = mActivitiesPerDisplay.get(displayId).getWindowManager() 450 .getCurrentWindowMetrics().getBounds(); 451 return new Point(rect.width() / 2, rect.height() / 2); 452 } 453 forEachPassengerMainDisplay(ThrowingBiConsumer<OccupantZoneInfo, Display> consumer)454 private void forEachPassengerMainDisplay(ThrowingBiConsumer<OccupantZoneInfo, Display> consumer) 455 throws Exception { 456 forEachMainDisplay(/* includesDriver= */ false, consumer); 457 } 458 forEachMainDisplay(boolean includesDriver, ThrowingBiConsumer<OccupantZoneInfo, Display> consumer)459 private void forEachMainDisplay(boolean includesDriver, 460 ThrowingBiConsumer<OccupantZoneInfo, Display> consumer) throws Exception { 461 assumeTrue("No passenger zones", mCarOccupantZoneManager.hasPassengerZones()); 462 List<OccupantZoneInfo> zones = mCarOccupantZoneManager.getAllOccupantZones(); 463 for (OccupantZoneInfo zone : zones) { 464 if (!includesDriver && zone.occupantType == OCCUPANT_TYPE_DRIVER) { 465 continue; 466 } 467 Display display = mCarOccupantZoneManager.getDisplayForOccupant(zone, 468 CarOccupantZoneManager.DISPLAY_TYPE_MAIN); 469 if (display == null) { 470 continue; 471 } 472 consumer.acceptOrThrow(zone, display); 473 } 474 } 475 476 /** 477 * A {@link BiConsumer} that allows throwing checked exceptions from its single abstract method. 478 * 479 * Can be used together with {@link #uncheckExceptions} to effectively turn a lambda expression 480 * that throws a checked exception into a regular {@link BiConsumer} 481 */ 482 @FunctionalInterface 483 @SuppressWarnings("FunctionalInterfaceMethodChanged") 484 public interface ThrowingBiConsumer<T, U> extends BiConsumer<T, U> { acceptOrThrow(T t, U u)485 void acceptOrThrow(T t, U u) throws Exception; 486 487 @Override accept(T t, U u)488 default void accept(T t, U u) { 489 try { 490 acceptOrThrow(t, u); 491 } catch (Exception ex) { 492 throw new RuntimeException(ex); 493 } 494 } 495 } 496 497 public static class TestActivity extends Activity { 498 private LinkedBlockingQueue<InputEvent> mEvents = new LinkedBlockingQueue<>(); 499 500 @Override dispatchTouchEvent(MotionEvent ev)501 public boolean dispatchTouchEvent(MotionEvent ev) { 502 mEvents.add(MotionEvent.obtain(ev)); 503 return true; 504 } 505 506 @Override dispatchKeyEvent(KeyEvent event)507 public boolean dispatchKeyEvent(KeyEvent event) { 508 mEvents.add(new KeyEvent(event)); 509 return true; 510 } 511 getInputEvent()512 public InputEvent getInputEvent() throws InterruptedException { 513 return mEvents.poll(DEFAULT_WAIT_MS, TimeUnit.MILLISECONDS); 514 } 515 assertNoEvents()516 public void assertNoEvents() throws InterruptedException { 517 InputEvent event = mEvents.poll(NO_EVENT_WAIT_MS, TimeUnit.MILLISECONDS); 518 assertWithMessage("Expected no events, but received %s", event).that(event).isNull(); 519 } 520 } 521 522 private static final class CarVolumeMonitor extends CarVolumeCallback { 523 // Copied from {@link android.car.CarOccupantZoneManager.OccupantZoneInfo#INVALID_ZONE_ID} 524 private static final int INVALID_ZONE_ID = -1; 525 // Copied from {@link android.car.media.CarAudioManager#INVALID_VOLUME_GROUP_ID} 526 private static final int INVALID_VOLUME_GROUP_ID = -1; 527 private final Object mLock = new Object(); 528 @GuardedBy("mLock") 529 private CountDownLatch mGroupVolumeChangeLatch = new CountDownLatch(1); 530 @GuardedBy("mLock") 531 private CountDownLatch mGroupMuteChangeLatch = new CountDownLatch(1); 532 533 public int zoneId = INVALID_ZONE_ID; 534 public int groupId = INVALID_VOLUME_GROUP_ID; 535 receivedGroupVolumeChanged(int zoneId)536 boolean receivedGroupVolumeChanged(int zoneId) throws InterruptedException { 537 CountDownLatch countDownLatch; 538 synchronized (mLock) { 539 countDownLatch = mGroupVolumeChangeLatch; 540 } 541 boolean succeed = countDownLatch.await(DEFAULT_WAIT_MS, TimeUnit.MILLISECONDS); 542 return succeed && this.zoneId == zoneId; 543 } 544 receivedGroupMuteChanged(int zoneId)545 boolean receivedGroupMuteChanged(int zoneId) throws InterruptedException { 546 CountDownLatch countDownLatch; 547 synchronized (mLock) { 548 countDownLatch = mGroupMuteChangeLatch; 549 } 550 boolean succeed = countDownLatch.await(DEFAULT_WAIT_MS, TimeUnit.MILLISECONDS); 551 return succeed && this.zoneId == zoneId; 552 } 553 reset()554 void reset() { 555 synchronized (mLock) { 556 mGroupVolumeChangeLatch = new CountDownLatch(1); 557 mGroupMuteChangeLatch = new CountDownLatch(1); 558 zoneId = INVALID_ZONE_ID; 559 groupId = INVALID_VOLUME_GROUP_ID; 560 } 561 } 562 563 @Override onGroupVolumeChanged(int zoneId, int groupId, int flags)564 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { 565 synchronized (mLock) { 566 this.zoneId = zoneId; 567 this.groupId = groupId; 568 mGroupVolumeChangeLatch.countDown(); 569 } 570 } 571 572 @Override onGroupMuteChanged(int zoneId, int groupId, int flags)573 public void onGroupMuteChanged(int zoneId, int groupId, int flags) { 574 synchronized (mLock) { 575 this.zoneId = zoneId; 576 this.groupId = groupId; 577 mGroupMuteChangeLatch.countDown(); 578 } 579 } 580 } 581 } 582