• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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