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