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.CarVolumeCallback; 23 24 import static androidx.lifecycle.Lifecycle.State.RESUMED; 25 import static androidx.lifecycle.Lifecycle.State.STARTED; 26 27 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 28 29 import static com.google.common.truth.Truth.assertThat; 30 import static com.google.common.truth.Truth.assertWithMessage; 31 32 import static org.junit.Assume.assumeTrue; 33 34 import android.app.Activity; 35 import android.app.ActivityOptions; 36 import android.car.Car; 37 import android.car.CarOccupantZoneManager; 38 import android.car.CarOccupantZoneManager.OccupantZoneInfo; 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.os.UserHandle; 44 import android.util.Pair; 45 import android.view.Display; 46 import android.view.InputEvent; 47 import android.view.KeyEvent; 48 import android.view.MotionEvent; 49 50 import androidx.test.core.app.ActivityScenario; 51 52 import com.android.bedstead.harrier.DeviceState; 53 import com.android.bedstead.harrier.annotations.RequireRunNotOnVisibleBackgroundNonProfileUser; 54 import com.android.compatibility.common.util.CddTest; 55 import com.android.compatibility.common.util.PollingCheck; 56 import com.android.internal.annotations.GuardedBy; 57 58 import org.junit.Before; 59 import org.junit.ClassRule; 60 import org.junit.Rule; 61 import org.junit.Test; 62 63 import java.util.Optional; 64 import java.util.concurrent.CountDownLatch; 65 import java.util.concurrent.LinkedBlockingQueue; 66 import java.util.concurrent.TimeUnit; 67 import java.util.concurrent.atomic.AtomicReference; 68 69 /** 70 * This test requires a device with one active passenger occupant. It also requires that the 71 * current user is the driver and not some passenger - this test won't work if 72 * `--user-type secondary_user_on_secondary_display` flag is passed. 73 */ 74 @RequireRunNotOnVisibleBackgroundNonProfileUser 75 public final class CarInputTest extends AbstractCarTestCase { 76 public static final String TAG = CarInputTest.class.getSimpleName(); 77 private static final long ACTIVITY_WAIT_TIME_OUT_MS = 10_000L; 78 private static final int DEFAULT_WAIT_MS = 5_000; 79 private static final int NO_EVENT_WAIT_MS = 100; 80 81 // Inject event commands. 82 private static final String OPTION_SEAT = "-s"; 83 private static final String OPTION_ACTION = "-a"; 84 private static final String OPTION_COUNT = "-c"; 85 private static final String OPTION_POINTER_ID = "-p"; 86 private static final String PREFIX_INJECTING_KEY_CMD = 87 "cmd car_service inject-key " + OPTION_SEAT + " %d %d"; 88 private static final String PREFIX_INJECTING_MOTION_CMD = "cmd car_service inject-motion"; 89 90 @ClassRule 91 @Rule 92 public static final DeviceState sDeviceState = new DeviceState(); 93 94 private CarOccupantZoneManager mCarOccupantZoneManager; 95 96 // Driver's associated occupant zone and display id. 97 private OccupantZoneInfo mDriverZoneInfo; 98 private int mDriverDisplayId; 99 100 // This field contains the occupant zone and display id for one randomly picked logged 101 // passenger. 102 private OccupantZoneInfo mPassengerZoneInfo; 103 private Display mPassengerDisplay; 104 105 @Before setUp()106 public void setUp() { 107 mCarOccupantZoneManager = getCar().getCarManager(CarOccupantZoneManager.class); 108 109 // Set the driver's zone and display and ensure this test is running with as the driver 110 // user. 111 var driverZoneAndDisplay = getDriverZoneAndDisplay(); 112 mDriverZoneInfo = driverZoneAndDisplay.first; 113 mDriverDisplayId = driverZoneAndDisplay.second.getDisplayId(); 114 115 // Set driver zone and display for one randomly chose passenger. 116 var anyPassengerZoneAndDisplay = pickAnyPassengerZoneAndDisplay(); 117 assumeTrue("This test requires (at least) one active passenger occupant", 118 anyPassengerZoneAndDisplay.isPresent()); 119 mPassengerZoneInfo = anyPassengerZoneAndDisplay.get().first; 120 mPassengerDisplay = anyPassengerZoneAndDisplay.get().second; 121 } 122 123 @Test 124 @CddTest(requirements = {"TODO(b/262236403)"}) testHomeKeyForAnyPassengerMainDisplay_bringsHomeForThePassengerDisplayOnly()125 public void testHomeKeyForAnyPassengerMainDisplay_bringsHomeForThePassengerDisplayOnly() { 126 // Launches TestActivity on both driver and passenger displays. 127 var intent = new Intent(mContext, TestActivity.class); 128 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 129 // Uses ShellPermission to launch an Activity on the different displays. 130 runWithShellPermissionIdentity(() -> { 131 try (ActivityScenario<TestActivity> driverActivityScenario = ActivityScenario.launch( 132 intent, 133 ActivityOptions.makeBasic().setLaunchDisplayId(mDriverDisplayId).toBundle())) { 134 try (ActivityScenario<TestActivity> passengerActivityScenario = 135 ActivityScenario.launch( 136 intent, 137 ActivityOptions.makeBasic().setLaunchDisplayId( 138 mPassengerDisplay.getDisplayId()).toBundle())) { 139 140 final var latch = new CountDownLatch(2); 141 driverActivityScenario.onActivity(unused -> latch.countDown()); 142 final var passengerActivity = new AtomicReference<TestActivity>(); 143 passengerActivityScenario.onActivity(a -> { 144 passengerActivity.set(a); 145 latch.countDown(); 146 }); 147 assertWithMessage("Waited for TestActivity to start on both " 148 + "driver and passenger displays.").that( 149 latch.await(ACTIVITY_WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue(); 150 151 doTestHomeKeyForAnyPassengerMainDisplay(driverActivityScenario, 152 passengerActivityScenario); 153 } 154 } 155 }); 156 } 157 doTestHomeKeyForAnyPassengerMainDisplay( ActivityScenario<TestActivity> driverActivityScenario, ActivityScenario<TestActivity> passengerActivityScenario)158 private void doTestHomeKeyForAnyPassengerMainDisplay( 159 ActivityScenario<TestActivity> driverActivityScenario, 160 ActivityScenario<TestActivity> passengerActivityScenario) { 161 162 injectKeyByShell(mPassengerZoneInfo, KeyEvent.KEYCODE_HOME); 163 164 // Verify that driver's activity wasn't affected by the HOME key event. 165 assertWithMessage("Home key should not affect the driver main display." 166 + " Home key was injected to display " 167 + mPassengerDisplay.getDisplayId() + ", but display " 168 + mDriverDisplayId + " was affected.") 169 .that(driverActivityScenario.getState()).isEqualTo(RESUMED); 170 171 // Verify that passenger's activity was affected by the HOME key event. 172 assertWithMessage("Home key should affect the passenger main display." 173 + " Home key was injected to display " 174 + mDriverDisplayId + ", but display wasn't affected (expected " 175 + "activity state to be STARTED, but instead it was " 176 + passengerActivityScenario.getState() + ")") 177 .that(passengerActivityScenario.getState()).isEqualTo(STARTED); 178 } 179 180 @Test 181 @CddTest(requirements = {"TODO(b/262236403)"}) testBackKeyForAnyPassengerMainDisplay()182 public void testBackKeyForAnyPassengerMainDisplay() { 183 // Start TestActivity on passenger's display. 184 var intent = new Intent(mContext, TestActivity.class); 185 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 186 // Uses ShellPermission to launch an Activity on the different displays. 187 runWithShellPermissionIdentity(() -> { 188 try (ActivityScenario<TestActivity> passengerActivityScenario = 189 ActivityScenario.launch( 190 intent, 191 ActivityOptions.makeBasic().setLaunchDisplayId( 192 mPassengerDisplay.getDisplayId()).toBundle())) { 193 final var latch = new CountDownLatch(1); 194 final var passengerActivity = new AtomicReference<TestActivity>(); 195 passengerActivityScenario.onActivity(a -> { 196 passengerActivity.set(a); 197 latch.countDown(); 198 }); 199 assertWithMessage("Waited for TestActivity to start on " 200 + "passenger displays.").that( 201 latch.await(ACTIVITY_WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue(); 202 203 int keyCode = KeyEvent.KEYCODE_BACK; 204 205 injectKeyByShell(mPassengerZoneInfo, keyCode); 206 207 assertReceivedKeyCode(passengerActivity.get(), mPassengerDisplay.getDisplayId(), 208 keyCode); 209 } 210 }); 211 } 212 213 @Test 214 @CddTest(requirements = {"TODO(b/262236403)"}) testAKeyForAnyPassengerMainDisplay()215 public void testAKeyForAnyPassengerMainDisplay() { 216 // Start TestActivity on passenger's display. 217 var intent = new Intent(mContext, TestActivity.class); 218 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 219 // Uses ShellPermission to launch an Activity on the different displays. 220 runWithShellPermissionIdentity(() -> { 221 try (ActivityScenario<TestActivity> passengerActivityScenario = 222 ActivityScenario.launch( 223 intent, 224 ActivityOptions.makeBasic().setLaunchDisplayId( 225 mPassengerDisplay.getDisplayId()).toBundle())) { 226 final var latch = new CountDownLatch(1); 227 final var passengerActivity = new AtomicReference<TestActivity>(); 228 passengerActivityScenario.onActivity(a -> { 229 passengerActivity.set(a); 230 latch.countDown(); 231 }); 232 assertWithMessage("Waited for TestActivity to start on " 233 + "passenger displays.").that( 234 latch.await(ACTIVITY_WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue(); 235 236 int keyCode = KeyEvent.KEYCODE_A; 237 injectKeyByShell(mPassengerZoneInfo, keyCode); 238 239 assertReceivedKeyCode(passengerActivity.get(), mPassengerDisplay.getDisplayId(), 240 keyCode); 241 } 242 }); 243 } 244 245 @Test 246 @CddTest(requirements = {"TODO(b/262236403)"}) testPowerKeyForAnyPassengerMainDisplay()247 public void testPowerKeyForAnyPassengerMainDisplay() { 248 // Start TestActivity on passenger's display. 249 var intent = new Intent(mContext, TestActivity.class); 250 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 251 // Uses ShellPermission to launch an Activity on the different displays. 252 runWithShellPermissionIdentity(() -> { 253 try (ActivityScenario<TestActivity> passengerActivityScenario = 254 ActivityScenario.launch( 255 intent, 256 ActivityOptions.makeBasic().setLaunchDisplayId( 257 mPassengerDisplay.getDisplayId()).toBundle())) { 258 final var latch = new CountDownLatch(1); 259 final var passengerActivity = new AtomicReference<TestActivity>(); 260 passengerActivityScenario.onActivity(a -> { 261 passengerActivity.set(a); 262 latch.countDown(); 263 }); 264 assertWithMessage("Waited for TestActivity to start on " 265 + "passenger displays.").that( 266 latch.await(ACTIVITY_WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue(); 267 268 // Screen off 269 int keyCode = KeyEvent.KEYCODE_POWER; 270 271 injectKeyByShell(mPassengerZoneInfo, keyCode); 272 PollingCheck.waitFor(DEFAULT_WAIT_MS, () -> { 273 return mPassengerDisplay.getState() == Display.STATE_OFF; 274 }, "Display state should be off"); 275 276 // Screen on 277 injectKeyByShell(mPassengerZoneInfo, keyCode); 278 PollingCheck.waitFor(DEFAULT_WAIT_MS, () -> { 279 return mPassengerDisplay.getState() == Display.STATE_ON; 280 }, "Display state should be on"); 281 } 282 }); 283 } 284 285 @Test 286 @CddTest(requirements = {"TODO(b/262236403)"}) 287 @EnsureHasPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) testVolumeUpKeyForAnyPassengerMainDisplay()288 public void testVolumeUpKeyForAnyPassengerMainDisplay() { 289 var audioManager = getCar().getCarManager(CarAudioManager.class); 290 assumeTrue(audioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)); 291 292 var callback = new CarVolumeMonitor(); 293 audioManager.registerCarVolumeCallback(callback); 294 295 // Start TestActivity on passenger's display. 296 var intent = new Intent(mContext, TestActivity.class); 297 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 298 // Uses ShellPermission to launch an Activity on the different displays. 299 try { 300 runWithShellPermissionIdentity(() -> { 301 try (ActivityScenario<TestActivity> passengerActivityScenario = 302 ActivityScenario.launch( 303 intent, 304 ActivityOptions.makeBasic().setLaunchDisplayId( 305 mPassengerDisplay.getDisplayId()).toBundle())) { 306 injectKeyByShell(mPassengerZoneInfo, 307 KeyEvent.KEYCODE_VOLUME_UP); 308 309 assertWithMessage("CarVolumeCallback#onGroupVolumeChanged should be called") 310 .that(callback.receivedGroupVolumeChanged( 311 mPassengerZoneInfo.zoneId)) 312 .isTrue(); 313 314 callback.reset(); 315 } 316 }); 317 } finally { 318 audioManager.unregisterCarVolumeCallback(callback); 319 } 320 } 321 322 @Test 323 @CddTest(requirements = {"TODO(b/262236403)"}) 324 @EnsureHasPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) testVolumeMuteKeyForAnyPassengerMainDisplay()325 public void testVolumeMuteKeyForAnyPassengerMainDisplay() { 326 var audioManager = getCar().getCarManager(CarAudioManager.class); 327 assumeTrue(audioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)); 328 329 var callback = new CarVolumeMonitor(); 330 audioManager.registerCarVolumeCallback(callback); 331 332 // Start TestActivity on passenger's display. 333 var intent = new Intent(mContext, TestActivity.class); 334 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 335 // Uses ShellPermission to launch an Activity on the different displays. 336 try { 337 runWithShellPermissionIdentity(() -> { 338 try (ActivityScenario<TestActivity> passengerActivityScenario = 339 ActivityScenario.launch( 340 intent, 341 ActivityOptions.makeBasic().setLaunchDisplayId( 342 mPassengerDisplay.getDisplayId()).toBundle())) { 343 injectKeyByShell(mPassengerZoneInfo, KeyEvent.KEYCODE_VOLUME_MUTE); 344 345 assertWithMessage("CarVolumeCallback#onMasterMuteChanged should be called") 346 .that(callback.receivedGroupMuteChanged(mPassengerZoneInfo.zoneId)) 347 .isTrue(); 348 assertThat( 349 audioManager.isVolumeGroupMuted(mPassengerZoneInfo.zoneId, 350 callback.mGroupId)) 351 .isTrue(); 352 callback.reset(); 353 } 354 }); 355 } finally { 356 audioManager.unregisterCarVolumeCallback(callback); 357 } 358 } 359 360 @Test 361 @CddTest(requirements = {"TODO(b/262236403)"}) testSingleTouchForAnyPassengerMainDisplay()362 public void testSingleTouchForAnyPassengerMainDisplay() { 363 // Start activity on both driver and passenger displays. 364 var intent = new Intent(mContext, TestActivity.class); 365 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 366 // Uses ShellPermission to launch an Activity on the different displays. 367 runWithShellPermissionIdentity(() -> { 368 try (ActivityScenario<TestActivity> driverActivityScenario = ActivityScenario.launch( 369 intent, 370 ActivityOptions.makeBasic().setLaunchDisplayId( 371 mDriverDisplayId).toBundle())) { 372 try (ActivityScenario<TestActivity> passengerActivityScenario = 373 ActivityScenario.launch( 374 intent, 375 ActivityOptions.makeBasic().setLaunchDisplayId( 376 mPassengerDisplay.getDisplayId()).toBundle())) { 377 378 final var latch = new CountDownLatch(2); 379 final var driverActivity = new AtomicReference<TestActivity>(); 380 driverActivityScenario.onActivity(a -> { 381 driverActivity.set(a); 382 latch.countDown(); 383 }); 384 final var passengerActivity = new AtomicReference<TestActivity>(); 385 passengerActivityScenario.onActivity(a -> { 386 passengerActivity.set(a); 387 latch.countDown(); 388 }); 389 assertWithMessage("Waited for TestActivity to start on both " 390 + "driver and passenger displays.").that( 391 latch.await(ACTIVITY_WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue(); 392 393 doTestSingleTouchForAnyPassenger(driverActivity.get(), passengerActivity.get()); 394 } 395 } 396 }); 397 } 398 doTestSingleTouchForAnyPassenger( TestActivity driverActivity, TestActivity passengerActivity)399 private void doTestSingleTouchForAnyPassenger( 400 TestActivity driverActivity, 401 TestActivity passengerActivity) throws InterruptedException { 402 403 var pointer = getDisplayCenter(passengerActivity); 404 405 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_DOWN, pointer); 406 assertReceivedMotionAction(passengerActivity, 407 mPassengerDisplay.getDisplayId(), 408 MotionEvent.ACTION_DOWN); 409 driverActivity.assertNoEvents(); 410 411 pointer.offset(1, 1); 412 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_MOVE, pointer); 413 assertReceivedMotionAction(passengerActivity, 414 mPassengerDisplay.getDisplayId(), 415 MotionEvent.ACTION_MOVE); 416 driverActivity.assertNoEvents(); 417 418 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_UP, pointer); 419 assertReceivedMotionAction(passengerActivity, 420 mPassengerDisplay.getDisplayId(), 421 MotionEvent.ACTION_UP); 422 driverActivity.assertNoEvents(); 423 } 424 425 @Test 426 @CddTest(requirements = {"TODO(b/262236403)"}) testMultiTouchForAnyPassengerMainDisplay()427 public void testMultiTouchForAnyPassengerMainDisplay() { 428 // Start activity on both driver and passenger displays. 429 var intent = new Intent(mContext, TestActivity.class); 430 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 431 // Uses ShellPermission to launch an Activity on the different displays. 432 runWithShellPermissionIdentity(() -> { 433 try (ActivityScenario<TestActivity> driverActivityScenario = ActivityScenario.launch( 434 intent, 435 ActivityOptions.makeBasic().setLaunchDisplayId( 436 mDriverDisplayId).toBundle())) { 437 438 try (ActivityScenario<TestActivity> passengerActivityScenario = 439 ActivityScenario.launch( 440 intent, 441 ActivityOptions.makeBasic().setLaunchDisplayId( 442 mPassengerDisplay.getDisplayId()).toBundle())) { 443 444 final var latch = new CountDownLatch(2); 445 final var driverActivity = new AtomicReference<TestActivity>(); 446 driverActivityScenario.onActivity(a -> { 447 driverActivity.set(a); 448 latch.countDown(); 449 }); 450 final var passengerActivity = new AtomicReference<TestActivity>(); 451 passengerActivityScenario.onActivity(a -> { 452 passengerActivity.set(a); 453 latch.countDown(); 454 }); 455 assertWithMessage("Waited for TestActivity to start on both " 456 + "driver and passenger displays.").that( 457 latch.await(ACTIVITY_WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS)).isTrue(); 458 459 doTestMultiTouchForAnyPassenger(driverActivity, passengerActivity); 460 } 461 } 462 }); 463 } 464 doTestMultiTouchForAnyPassenger(AtomicReference<TestActivity> driverActivity, AtomicReference<TestActivity> passengerActivity)465 private void doTestMultiTouchForAnyPassenger(AtomicReference<TestActivity> driverActivity, 466 AtomicReference<TestActivity> passengerActivity) throws InterruptedException { 467 var pointer1 = getDisplayCenter(passengerActivity.get()); 468 var pointer2 = getDisplayCenter(passengerActivity.get()); 469 pointer2.offset(100, 100); 470 var pointers = new Point[]{pointer1, pointer2}; 471 472 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_DOWN, pointer1); 473 assertReceivedMotionAction(passengerActivity.get(), 474 mPassengerDisplay.getDisplayId(), 475 MotionEvent.ACTION_DOWN); 476 driverActivity.get().assertNoEvents(); 477 478 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_POINTER_DOWN, 479 pointers); 480 assertReceivedMotionAction(passengerActivity.get(), 481 mPassengerDisplay.getDisplayId(), 482 MotionEvent.ACTION_POINTER_DOWN); 483 driverActivity.get().assertNoEvents(); 484 485 pointer2.offset(1, 1); 486 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_MOVE, pointers); 487 assertReceivedMotionAction(passengerActivity.get(), 488 mPassengerDisplay.getDisplayId(), 489 MotionEvent.ACTION_MOVE); 490 driverActivity.get().assertNoEvents(); 491 492 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_POINTER_UP, pointers); 493 assertReceivedMotionAction(passengerActivity.get(), 494 mPassengerDisplay.getDisplayId(), 495 MotionEvent.ACTION_POINTER_UP); 496 driverActivity.get().assertNoEvents(); 497 498 injectTouchByShell(mPassengerZoneInfo, MotionEvent.ACTION_UP, pointer1); 499 assertReceivedMotionAction(passengerActivity.get(), 500 mPassengerDisplay.getDisplayId(), 501 MotionEvent.ACTION_UP); 502 driverActivity.get().assertNoEvents(); 503 } 504 getDriverZoneAndDisplay()505 private Pair<OccupantZoneInfo, Display> getDriverZoneAndDisplay() { 506 var zones = 507 mCarOccupantZoneManager.getAllOccupantZones().stream().filter( 508 o -> o.occupantType == OCCUPANT_TYPE_DRIVER).toList(); 509 assertWithMessage("Expected occupant zones to contain the driver occupant zone").that( 510 zones).hasSize(1); 511 var driverZone = zones.get(0); 512 var display = mCarOccupantZoneManager.getDisplayForOccupant(driverZone, 513 CarOccupantZoneManager.DISPLAY_TYPE_MAIN); 514 return new Pair<>(driverZone, display); 515 } 516 pickAnyPassengerZoneAndDisplay()517 private Optional<Pair<OccupantZoneInfo, Display>> pickAnyPassengerZoneAndDisplay() { 518 var zones = 519 mCarOccupantZoneManager.getAllOccupantZones().stream().filter( 520 o -> o.occupantType != OCCUPANT_TYPE_DRIVER 521 && mCarOccupantZoneManager.getUserForOccupant(o) 522 != UserHandle.USER_NULL).toList(); 523 if (zones.isEmpty()) { 524 return Optional.empty(); 525 } 526 var passengerZone = zones.get(0); 527 var display = mCarOccupantZoneManager.getDisplayForOccupant(passengerZone, 528 CarOccupantZoneManager.DISPLAY_TYPE_MAIN); 529 return Optional.of(new Pair<>(passengerZone, display)); 530 } 531 injectKeyByShell(OccupantZoneInfo zone, int keyCode)532 private static void injectKeyByShell(OccupantZoneInfo zone, int keyCode) { 533 String command = String.format(PREFIX_INJECTING_KEY_CMD, zone.seat, keyCode); 534 runShellCommand(command); 535 } 536 assertReceivedKeyCode(TestActivity passengerActivity, int passengerDisplayId, int keyCode)537 private void assertReceivedKeyCode(TestActivity passengerActivity, int passengerDisplayId, 538 int keyCode) { 539 var downEvent = passengerActivity.getInputEvent(); 540 var upEvent = passengerActivity.getInputEvent(); 541 assertWithMessage("Activity on display " + passengerDisplayId 542 + " must receive key event, keyCode=" 543 + KeyEvent.keyCodeToString(keyCode)) 544 .that(downEvent instanceof KeyEvent).isTrue(); 545 assertWithMessage("Activity on display " + passengerDisplayId 546 + " must receive key event, keyCode=" 547 + KeyEvent.keyCodeToString(keyCode)) 548 .that(upEvent instanceof KeyEvent).isTrue(); 549 assertWithMessage("Activity on display " + passengerDisplayId 550 + " must receive " + KeyEvent.keyCodeToString(keyCode)) 551 .that(((KeyEvent) downEvent).getKeyCode()).isEqualTo(keyCode); 552 assertWithMessage("Activity on display " + passengerDisplayId 553 + " must receive down event, keyCode=" + KeyEvent.keyCodeToString(keyCode)) 554 .that(((KeyEvent) downEvent).getAction()).isEqualTo(KeyEvent.ACTION_DOWN); 555 assertWithMessage("Activity on display " + passengerDisplayId 556 + " must receive " + KeyEvent.keyCodeToString(keyCode)) 557 .that(((KeyEvent) upEvent).getKeyCode()).isEqualTo(keyCode); 558 assertWithMessage("Activity on display " + passengerDisplayId 559 + " must receive up event, keyCode=" + KeyEvent.keyCodeToString(keyCode)) 560 .that(((KeyEvent) upEvent).getAction()).isEqualTo(KeyEvent.ACTION_UP); 561 } 562 assertReceivedMotionAction(TestActivity activity, int displayId, int actionMasked)563 private void assertReceivedMotionAction(TestActivity activity, int displayId, 564 int actionMasked) { 565 var event = activity.getInputEvent(); 566 assertWithMessage("Activity on display " + displayId + " must receive motion event, action=" 567 + MotionEvent.actionToString(actionMasked)) 568 .that(event instanceof MotionEvent).isTrue(); 569 var motionEvent = (MotionEvent) event; 570 assertWithMessage("Activity on display " + displayId 571 + " must receive " + MotionEvent.actionToString(actionMasked)) 572 .that(motionEvent.getActionMasked()).isEqualTo(actionMasked); 573 } 574 injectTouchByShell(OccupantZoneInfo zone, int action, Point p)575 private static void injectTouchByShell(OccupantZoneInfo zone, int action, Point p) { 576 injectTouchByShell(zone, action, new Point[]{p}); 577 } 578 injectTouchByShell(OccupantZoneInfo zone, int action, Point[] p)579 private static void injectTouchByShell(OccupantZoneInfo zone, int action, Point[] p) { 580 int pointerCount = p.length; 581 if (action == MotionEvent.ACTION_POINTER_DOWN || action == MotionEvent.ACTION_POINTER_UP) { 582 int index = p.length - 1; 583 action = (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT) + action; 584 } 585 586 // Generate a command message 587 var sb = new StringBuilder() 588 .append(PREFIX_INJECTING_MOTION_CMD) 589 .append(' ').append(OPTION_SEAT) 590 .append(' ').append(zone.seat) 591 .append(' ').append(OPTION_ACTION) 592 .append(' ').append(action) 593 .append(' ').append(OPTION_COUNT) 594 .append(' ').append(pointerCount); 595 sb.append(' ').append(OPTION_POINTER_ID); 596 for (int i = 0; i < pointerCount; i++) { 597 sb.append(' '); 598 sb.append(i); 599 } 600 for (int i = 0; i < pointerCount; i++) { 601 sb.append(' '); 602 sb.append(p[i].x); 603 sb.append(' '); 604 sb.append(p[i].y); 605 } 606 runShellCommand(sb.toString()); 607 } 608 getDisplayCenter(TestActivity activity)609 private Point getDisplayCenter(TestActivity activity) { 610 var rect = activity.getWindowManager().getCurrentWindowMetrics().getBounds(); 611 return new Point(rect.width() / 2, rect.height() / 2); 612 } 613 614 public static class TestActivity extends Activity { 615 private LinkedBlockingQueue<InputEvent> mEvents = new LinkedBlockingQueue<>(); 616 public boolean mPaused = false; 617 618 @Override onPause()619 protected void onPause() { 620 super.onPause(); 621 mPaused = true; 622 } 623 624 @Override dispatchTouchEvent(MotionEvent ev)625 public boolean dispatchTouchEvent(MotionEvent ev) { 626 mEvents.add(MotionEvent.obtain(ev)); 627 return true; 628 } 629 630 @Override dispatchKeyEvent(KeyEvent event)631 public boolean dispatchKeyEvent(KeyEvent event) { 632 mEvents.add(new KeyEvent(event)); 633 return true; 634 } 635 getInputEvent()636 public InputEvent getInputEvent() { 637 try { 638 return mEvents.poll(DEFAULT_WAIT_MS, TimeUnit.MILLISECONDS); 639 } catch (InterruptedException e) { 640 Thread.currentThread().interrupt(); 641 throw new RuntimeException(e); 642 } 643 } 644 assertNoEvents()645 public void assertNoEvents() throws InterruptedException { 646 InputEvent event = mEvents.poll(NO_EVENT_WAIT_MS, TimeUnit.MILLISECONDS); 647 assertWithMessage("Expected no events, but received %s", event).that( 648 event).isNull(); 649 } 650 } 651 652 private static final class CarVolumeMonitor extends CarVolumeCallback { 653 // Copied from {@link android.car.CarOccupantZoneManager.OccupantZoneInfo#INVALID_ZONE_ID} 654 private static final int INVALID_ZONE_ID = -1; 655 // Copied from {@link android.car.media.CarAudioManager#INVALID_VOLUME_GROUP_ID} 656 private static final int INVALID_VOLUME_GROUP_ID = -1; 657 private final Object mLock = new Object(); 658 @GuardedBy("mLock") 659 private CountDownLatch mGroupVolumeChangeLatch = new CountDownLatch(1); 660 @GuardedBy("mLock") 661 private CountDownLatch mGroupMuteChangeLatch = new CountDownLatch(1); 662 663 public int mZoneId = INVALID_ZONE_ID; 664 public int mGroupId = INVALID_VOLUME_GROUP_ID; 665 receivedGroupVolumeChanged(int zoneId)666 boolean receivedGroupVolumeChanged(int zoneId) throws InterruptedException { 667 CountDownLatch countDownLatch; 668 synchronized (mLock) { 669 countDownLatch = mGroupVolumeChangeLatch; 670 } 671 boolean succeed = countDownLatch.await(DEFAULT_WAIT_MS, TimeUnit.MILLISECONDS); 672 return succeed && this.mZoneId == zoneId; 673 } 674 receivedGroupMuteChanged(int zoneId)675 boolean receivedGroupMuteChanged(int zoneId) throws InterruptedException { 676 CountDownLatch countDownLatch; 677 synchronized (mLock) { 678 countDownLatch = mGroupMuteChangeLatch; 679 } 680 boolean succeed = countDownLatch.await(DEFAULT_WAIT_MS, TimeUnit.MILLISECONDS); 681 return succeed && this.mZoneId == zoneId; 682 } 683 reset()684 void reset() { 685 synchronized (mLock) { 686 mGroupVolumeChangeLatch = new CountDownLatch(1); 687 mGroupMuteChangeLatch = new CountDownLatch(1); 688 mZoneId = INVALID_ZONE_ID; 689 mGroupId = INVALID_VOLUME_GROUP_ID; 690 } 691 } 692 693 @Override onGroupVolumeChanged(int zoneId, int groupId, int flags)694 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { 695 synchronized (mLock) { 696 mZoneId = zoneId; 697 mGroupId = groupId; 698 mGroupVolumeChangeLatch.countDown(); 699 } 700 } 701 702 @Override onGroupMuteChanged(int zoneId, int groupId, int flags)703 public void onGroupMuteChanged(int zoneId, int groupId, int flags) { 704 synchronized (mLock) { 705 mZoneId = zoneId; 706 mGroupId = groupId; 707 mGroupMuteChangeLatch.countDown(); 708 } 709 } 710 } 711 } 712