• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 package com.android.car.hal;
17 
18 import static android.car.CarOccupantZoneManager.DisplayTypeEnum;
19 import static android.hardware.automotive.vehicle.CustomInputType.CUSTOM_EVENT_F1;
20 
21 import static com.android.car.CarServiceUtils.toIntArray;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.ArgumentMatchers.anyInt;
27 import static org.mockito.ArgumentMatchers.eq;
28 import static org.mockito.Mockito.doAnswer;
29 import static org.mockito.Mockito.never;
30 import static org.mockito.Mockito.reset;
31 import static org.mockito.Mockito.times;
32 import static org.mockito.Mockito.verify;
33 import static org.mockito.Mockito.when;
34 
35 import android.car.CarOccupantZoneManager;
36 import android.car.input.CarInputManager;
37 import android.car.input.CustomInputEvent;
38 import android.car.input.RotaryEvent;
39 import android.hardware.automotive.vehicle.RotaryInputType;
40 import android.hardware.automotive.vehicle.VehicleDisplay;
41 import android.hardware.automotive.vehicle.VehicleHwKeyInputAction;
42 import android.hardware.automotive.vehicle.VehicleProperty;
43 import android.view.Display;
44 import android.view.KeyEvent;
45 
46 import androidx.test.filters.RequiresDevice;
47 
48 import com.android.car.hal.test.AidlVehiclePropConfigBuilder;
49 
50 import org.junit.After;
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 import org.mockito.ArgumentCaptor;
55 import org.mockito.Mock;
56 import org.mockito.junit.MockitoJUnitRunner;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 import java.util.Set;
61 import java.util.function.LongSupplier;
62 
63 @RunWith(MockitoJUnitRunner.class)
64 public class InputHalServiceTest {
65     @Mock VehicleHal mVehicleHal;
66     @Mock InputHalService.InputListener mInputListener;
67     @Mock LongSupplier mUptimeSupplier;
68 
69     private static final HalPropConfig HW_KEY_INPUT_CONFIG = new AidlHalPropConfig(
70             AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_KEY_INPUT).build());
71     private static final HalPropConfig HW_ROTARY_INPUT_CONFIG = new AidlHalPropConfig(
72             AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_ROTARY_INPUT).build());
73     private static final HalPropConfig HW_CUSTOM_INPUT_CONFIG = new AidlHalPropConfig(
74             AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_CUSTOM_INPUT).build());
75 
76     private final HalPropValueBuilder mPropValueBuilder = new HalPropValueBuilder(/*isAidl=*/true);
77 
78     private enum Key {DOWN, UP}
79 
80     private InputHalService mInputHalService;
81 
82     @Before
setUp()83     public void setUp() {
84         when(mUptimeSupplier.getAsLong()).thenReturn(0L);
85         mInputHalService = new InputHalService(mVehicleHal, mUptimeSupplier);
86         mInputHalService.init();
87     }
88 
89     @After
tearDown()90     public void tearDown() {
91         mInputHalService.release();
92         mInputHalService = null;
93     }
94 
95     @Test
ignoresSetListener_beforeKeyInputSupported()96     public void ignoresSetListener_beforeKeyInputSupported() {
97         assertThat(mInputHalService.isKeyInputSupported()).isFalse();
98 
99         mInputHalService.setInputListener(mInputListener);
100 
101         int anyDisplay = VehicleDisplay.MAIN;
102         mInputHalService.onHalEvents(List.of(makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER,
103                 anyDisplay)));
104         verify(mInputListener, never()).onKeyEvent(any(), anyInt());
105     }
106 
107     @Test
takesKeyInputProperty()108     public void takesKeyInputProperty() {
109         Set<HalPropConfig> offeredProps = Set.of(new AidlHalPropConfig(
110                 AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build()),
111                 HW_KEY_INPUT_CONFIG,
112                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
113                         VehicleProperty.CURRENT_GEAR).build()));
114 
115         mInputHalService.takeProperties(offeredProps);
116 
117         assertThat(mInputHalService.isKeyInputSupported()).isTrue();
118         assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
119         assertThat(mInputHalService.isCustomInputSupported()).isFalse();
120     }
121 
122     @Test
takesRotaryInputProperty()123     public void takesRotaryInputProperty() {
124         Set<HalPropConfig> offeredProps = Set.of(
125                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
126                         VehicleProperty.ABS_ACTIVE).build()),
127                 HW_ROTARY_INPUT_CONFIG,
128                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
129                         VehicleProperty.CURRENT_GEAR).build()));
130 
131         mInputHalService.takeProperties(offeredProps);
132 
133         assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
134         assertThat(mInputHalService.isKeyInputSupported()).isFalse();
135         assertThat(mInputHalService.isCustomInputSupported()).isFalse();
136     }
137 
138     @Test
takesCustomInputProperty()139     public void takesCustomInputProperty() {
140         Set<HalPropConfig> offeredProps = Set.of(
141                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
142                         VehicleProperty.ABS_ACTIVE).build()),
143                 HW_CUSTOM_INPUT_CONFIG,
144                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
145                         VehicleProperty.CURRENT_GEAR).build()));
146 
147         mInputHalService.takeProperties(offeredProps);
148 
149         assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
150         assertThat(mInputHalService.isKeyInputSupported()).isFalse();
151         assertThat(mInputHalService.isCustomInputSupported()).isTrue();
152     }
153 
154     @Test
takesKeyAndRotaryAndCustomInputProperty()155     public void takesKeyAndRotaryAndCustomInputProperty() {
156         Set<HalPropConfig> offeredProps = Set.of(
157                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
158                         VehicleProperty.ABS_ACTIVE).build()),
159                 HW_KEY_INPUT_CONFIG,
160                 HW_ROTARY_INPUT_CONFIG,
161                 HW_CUSTOM_INPUT_CONFIG,
162                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
163                         VehicleProperty.CURRENT_GEAR).build()));
164 
165         mInputHalService.takeProperties(offeredProps);
166 
167         assertThat(mInputHalService.isKeyInputSupported()).isTrue();
168         assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
169         assertThat(mInputHalService.isCustomInputSupported()).isTrue();
170     }
171 
172     @Test
dispatchesInputEvent_single_toListener_mainDisplay()173     public void dispatchesInputEvent_single_toListener_mainDisplay() {
174         subscribeListener();
175 
176         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
177                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
178         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
179         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
180     }
181 
182     @Test
dispatchesInputEvent_single_toListener_clusterDisplay()183     public void dispatchesInputEvent_single_toListener_clusterDisplay() {
184         subscribeListener();
185 
186         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER,
187                 VehicleDisplay.INSTRUMENT_CLUSTER,
188                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
189         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
190         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
191     }
192 
193     @Test
dispatchesInputEvent_multiple_toListener_mainDisplay()194     public void dispatchesInputEvent_multiple_toListener_mainDisplay() {
195         subscribeListener();
196 
197         // KeyEvents get recycled, so we can't just use ArgumentCaptor#getAllValues here.
198         // We need to make a copy of the information we need at the time of the call.
199         List<KeyEvent> events = new ArrayList<>();
200         doAnswer(inv -> {
201             KeyEvent event = inv.getArgument(0);
202             events.add(event.copy());
203             return null;
204         }).when(mInputListener).onKeyEvent(any(), eq(CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
205 
206         mInputHalService.onHalEvents(
207                 List.of(
208                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN),
209                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_MENU, VehicleDisplay.MAIN)));
210 
211         assertThat(events.get(0).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
212         assertThat(events.get(1).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
213 
214         events.forEach(KeyEvent::recycle);
215     }
216 
217     @Test
dispatchesInputEvent_multiple_toListener_clusterDisplay()218     public void dispatchesInputEvent_multiple_toListener_clusterDisplay() {
219         subscribeListener();
220 
221         // KeyEvents get recycled, so we can't just use ArgumentCaptor#getAllValues here.
222         // We need to make a copy of the information we need at the time of the call.
223         List<KeyEvent> events = new ArrayList<>();
224         doAnswer(inv -> {
225             KeyEvent event = inv.getArgument(0);
226             events.add(event.copy());
227             return null;
228         }).when(mInputListener).onKeyEvent(any(),
229                 eq(CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER));
230 
231         mInputHalService.onHalEvents(
232                 List.of(
233                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER,
234                                 VehicleDisplay.INSTRUMENT_CLUSTER),
235                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_MENU,
236                                 VehicleDisplay.INSTRUMENT_CLUSTER)));
237 
238         assertThat(events.get(0).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
239         assertThat(events.get(1).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
240 
241         events.forEach(KeyEvent::recycle);
242     }
243 
244     @Test
dispatchesInputEvent_invalidInputEvent()245     public void dispatchesInputEvent_invalidInputEvent() {
246         subscribeListener();
247         HalPropValue v = mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0);
248         // Missing action, code, display_type.
249         mInputHalService.onHalEvents(List.of(v));
250         verify(mInputListener, never()).onKeyEvent(any(),  anyInt());
251 
252         // Missing code, display_type.
253         v = mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
254                 VehicleHwKeyInputAction.ACTION_DOWN);
255         mInputHalService.onHalEvents(List.of(v));
256         verify(mInputListener, never()).onKeyEvent(any(),  anyInt());
257 
258         // Missing display_type.
259         v = mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
260                 new int[]{VehicleHwKeyInputAction.ACTION_DOWN, KeyEvent.KEYCODE_ENTER});
261         mInputHalService.onHalEvents(List.of(v));
262         verify(mInputListener, never()).onKeyEvent(any(),  anyInt());
263     }
264 
265     @Test
handlesRepeatedKeys_anyDisplay()266     public void handlesRepeatedKeys_anyDisplay() {
267         subscribeListener();
268 
269         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
270                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
271         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
272         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
273         assertThat(event.getEventTime()).isEqualTo(0L);
274         assertThat(event.getDownTime()).isEqualTo(0L);
275         assertThat(event.getRepeatCount()).isEqualTo(0);
276 
277         when(mUptimeSupplier.getAsLong()).thenReturn(5L);
278         event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
279                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
280 
281         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
282         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
283         assertThat(event.getEventTime()).isEqualTo(5L);
284         assertThat(event.getDownTime()).isEqualTo(5L);
285         assertThat(event.getRepeatCount()).isEqualTo(1);
286 
287         when(mUptimeSupplier.getAsLong()).thenReturn(10L);
288         event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
289                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
290 
291         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_UP);
292         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
293         assertThat(event.getEventTime()).isEqualTo(10L);
294         assertThat(event.getDownTime()).isEqualTo(5L);
295         assertThat(event.getRepeatCount()).isEqualTo(0);
296 
297         when(mUptimeSupplier.getAsLong()).thenReturn(15L);
298         event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
299                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
300 
301         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
302         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
303         assertThat(event.getEventTime()).isEqualTo(15L);
304         assertThat(event.getDownTime()).isEqualTo(15L);
305         assertThat(event.getRepeatCount()).isEqualTo(0);
306     }
307 
308     /**
309      * Test for handling rotary knob event.
310      */
311     @RequiresDevice
312     @Test
handlesRepeatedKeyWithIndents_anyDisplay()313     public void handlesRepeatedKeyWithIndents_anyDisplay() {
314         subscribeListener();
315         KeyEvent event = dispatchSingleEventWithIndents(KeyEvent.KEYCODE_VOLUME_UP, 5,
316                 VehicleDisplay.MAIN, CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
317         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
318         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
319         assertThat(event.getEventTime()).isEqualTo(0L);
320         assertThat(event.getDownTime()).isEqualTo(0L);
321         assertThat(event.getRepeatCount()).isEqualTo(4);
322 
323         when(mUptimeSupplier.getAsLong()).thenReturn(5L);
324         event = dispatchSingleEventWithIndents(KeyEvent.KEYCODE_VOLUME_UP, 5,
325                 VehicleDisplay.MAIN, CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
326         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
327         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
328         assertThat(event.getEventTime()).isEqualTo(5L);
329         assertThat(event.getDownTime()).isEqualTo(5L);
330         assertThat(event.getRepeatCount()).isEqualTo(9);
331     }
332 
333     @Test
handlesKeyUp_withoutKeyDown_mainDisplay()334     public void handlesKeyUp_withoutKeyDown_mainDisplay() {
335         subscribeListener();
336 
337         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
338         KeyEvent event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
339                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
340 
341         assertThat(event.getEventTime()).isEqualTo(42L);
342         assertThat(event.getDownTime()).isEqualTo(42L);
343         assertThat(event.getRepeatCount()).isEqualTo(0);
344         assertThat(event.getDisplayId()).isEqualTo(Display.DEFAULT_DISPLAY);
345     }
346 
347     @Test
handlesKeyUp_withoutKeyDown_clusterDisplay()348     public void handlesKeyUp_withoutKeyDown_clusterDisplay() {
349         subscribeListener();
350 
351         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
352         KeyEvent event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER,
353                 VehicleDisplay.INSTRUMENT_CLUSTER,
354                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
355 
356         assertThat(event.getEventTime()).isEqualTo(42L);
357         assertThat(event.getDownTime()).isEqualTo(42L);
358         assertThat(event.getRepeatCount()).isEqualTo(0);
359         // event.getDisplayId is not tested since it is assigned by CarInputService
360     }
361 
362     @Test
separateKeyDownEvents_areIndependent_mainDisplay()363     public void separateKeyDownEvents_areIndependent_mainDisplay() {
364         subscribeListener();
365 
366         when(mUptimeSupplier.getAsLong()).thenReturn(27L);
367         dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
368                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
369 
370         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
371         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_MENU, VehicleDisplay.MAIN,
372                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
373 
374         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
375         assertThat(event.getDownTime()).isEqualTo(42L);
376         assertThat(event.getRepeatCount()).isEqualTo(0);
377     }
378 
379     @Test
separateKeyDownEvents_areIndependent_clusterDisplay()380     public void separateKeyDownEvents_areIndependent_clusterDisplay() {
381         subscribeListener();
382 
383         when(mUptimeSupplier.getAsLong()).thenReturn(27L);
384         dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.INSTRUMENT_CLUSTER,
385                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
386 
387         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
388         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_MENU, VehicleDisplay.MAIN,
389                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
390 
391         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
392         assertThat(event.getDownTime()).isEqualTo(42L);
393         assertThat(event.getRepeatCount()).isEqualTo(0);
394         // event.getDisplayid is not tested since it is assigned by CarInputService
395     }
396 
397     @Test
dispatchesRotaryEvent_singleVolumeUp_anyDisplay()398     public void dispatchesRotaryEvent_singleVolumeUp_anyDisplay() {
399         subscribeListener();
400 
401         // Arrange mInputListener to capture incoming RotaryEvent
402         List<RotaryEvent> events = new ArrayList<>();
403         doAnswer(invocation -> {
404             RotaryEvent event = invocation.getArgument(0);
405             events.add(event);
406             return null;
407         }).when(mInputListener).onRotaryEvent(any(), eq(CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
408 
409         // Arrange
410         long timestampNanos = 12_345_678_901L;
411 
412         // Act
413         mInputHalService.onHalEvents(List.of(
414                 makeRotaryPropValue(RotaryInputType.ROTARY_INPUT_TYPE_AUDIO_VOLUME, 1,
415                         timestampNanos, 0, VehicleDisplay.MAIN)));
416 
417         // Assert
418 
419         // Expected Rotary event to have only one value for uptimeMillisForClicks since the input
420         // property was created with one detent only. This value will correspond to the event
421         // startup time. See CarServiceUtils#getUptimeToElapsedTimeDeltaInMillis for more detailed
422         // information on how this value is calculated.
423         assertThat(events).containsExactly(new RotaryEvent(
424                 /* inputType= */ CarInputManager.INPUT_TYPE_ROTARY_VOLUME,
425                 /* clockwise= */ true,
426                 /* uptimeMillisForClicks= */ new long[]{12345L}));
427     }
428 
429     @Test
dispatchesRotaryEvent_multipleNavigatePrevious_anyDisplay()430     public void dispatchesRotaryEvent_multipleNavigatePrevious_anyDisplay() {
431         subscribeListener();
432 
433         // Arrange mInputListener to capture incoming RotaryEvent
434         List<RotaryEvent> events = new ArrayList<>();
435         doAnswer(invocation -> {
436             RotaryEvent event = invocation.getArgument(0);
437             events.add(event);
438             return null;
439         }).when(mInputListener).onRotaryEvent(any(), eq(CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
440 
441         // Arrange
442         long timestampNanos = 12_345_000_000L;
443         int deltaNanos = 2_000_000;
444         int numberOfDetents = 3;
445 
446         // Act
447         mInputHalService.onHalEvents(List.of(
448                 makeRotaryPropValue(RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION,
449                         -numberOfDetents, timestampNanos, deltaNanos, VehicleDisplay.MAIN)));
450 
451         // Assert
452 
453         // Expected Rotary event to have 3 values for uptimeMillisForClicks since the input
454         // property value was created with 3 detents. Each value in uptimeMillisForClicks
455         // represents the calculated deltas (in nanoseconds) between pairs of consecutive detents
456         // up times. See InputHalService#dispatchRotaryInput for more detailed information on how
457         // delta times are calculated.
458         assertThat(events).containsExactly(new RotaryEvent(
459                 /* inputType= */CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
460                 /* clockwise= */ false,
461                 /* uptimeMillisForClicks= */ new long[]{12345L, 12347L, 12349L}));
462     }
463 
464     @Test
dispatchesRotaryEvent_invalidInputEvent()465     public void dispatchesRotaryEvent_invalidInputEvent() {
466         subscribeListener();
467         HalPropValue v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0);
468 
469         // Missing rotaryInputType, detentCount, targetDisplayType.
470         mInputHalService.onHalEvents(List.of(v));
471         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
472 
473         // Missing detentCount, targetDisplayType.
474         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0,
475                 RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION);
476         mInputHalService.onHalEvents(List.of(v));
477         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
478 
479         // Missing targetDisplayType.
480         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0,
481                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, 1});
482         mInputHalService.onHalEvents(List.of(v));
483         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
484 
485         // Add targetDisplayType and set detentCount to 0.
486         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0,
487                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, 0,
488                         VehicleDisplay.MAIN});
489         mInputHalService.onHalEvents(List.of(v));
490         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
491 
492         // Set detentCount to 1.
493         // Add additional unnecessary arguments so that the array size does not match detentCount.
494         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 1,
495                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, 1,
496                         VehicleDisplay.MAIN, 0});
497         mInputHalService.onHalEvents(List.of(v));
498         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
499 
500         // Set invalid detentCount.
501         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 1,
502                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, Integer.MAX_VALUE,
503                         VehicleDisplay.MAIN, 0});
504         mInputHalService.onHalEvents(List.of(v));
505         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
506 
507         // Set invalid detentCount.
508         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 1,
509                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, Integer.MIN_VALUE,
510                         VehicleDisplay.MAIN, 0});
511         mInputHalService.onHalEvents(List.of(v));
512         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
513     }
514 
515     @Test
dispatchesCustomInputEvent_mainDisplay()516     public void dispatchesCustomInputEvent_mainDisplay() {
517         // Arrange mInputListener to capture incoming CustomInputEvent
518         subscribeListener();
519 
520         List<CustomInputEvent> events = new ArrayList<>();
521         doAnswer(invocation -> {
522             CustomInputEvent event = invocation.getArgument(0);
523             events.add(event);
524             return null;
525         }).when(mInputListener).onCustomInputEvent(any());
526 
527         // Arrange
528         int repeatCounter = 1;
529         HalPropValue customInputPropValue = makeCustomInputPropValue(
530                 CUSTOM_EVENT_F1, VehicleDisplay.MAIN, repeatCounter);
531 
532         // Act
533         mInputHalService.onHalEvents(List.of(customInputPropValue));
534 
535         // Assert
536         assertThat(events).containsExactly(new CustomInputEvent(
537                 CustomInputEvent.INPUT_CODE_F1, CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
538                 repeatCounter));
539     }
540 
541     @Test
dispatchesCustomInputEvent_clusterDisplay()542     public void dispatchesCustomInputEvent_clusterDisplay() {
543         // Arrange mInputListener to capture incoming CustomInputEvent
544         subscribeListener();
545 
546         List<CustomInputEvent> events = new ArrayList<>();
547         doAnswer(invocation -> {
548             CustomInputEvent event = invocation.getArgument(0);
549             events.add(event);
550             return null;
551         }).when(mInputListener).onCustomInputEvent(any());
552 
553         // Arrange
554         int repeatCounter = 1;
555         HalPropValue customInputPropValue = makeCustomInputPropValue(
556                 CUSTOM_EVENT_F1, VehicleDisplay.INSTRUMENT_CLUSTER, repeatCounter);
557 
558         // Act
559         mInputHalService.onHalEvents(List.of(customInputPropValue));
560 
561         // Assert
562         assertThat(events).containsExactly(new CustomInputEvent(
563                 CustomInputEvent.INPUT_CODE_F1,
564                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER,
565                 repeatCounter));
566     }
567 
568     @Test
dispatchesCustomInputEvent_InvalidEvent()569     public void dispatchesCustomInputEvent_InvalidEvent() {
570         // Arrange mInputListener to capture incoming CustomInputEvent
571         subscribeListener();
572 
573         HalPropValue v = mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0);
574 
575         // Missing inputCode, targetDisplayType, repeatCounter.
576         mInputHalService.onHalEvents(List.of(v));
577         verify(mInputListener, never()).onCustomInputEvent(any());
578 
579         // Missing targetDisplayType, repeatCounter.
580         v = mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0,
581                 CustomInputEvent.INPUT_CODE_F1);
582         mInputHalService.onHalEvents(List.of(v));
583         verify(mInputListener, never()).onCustomInputEvent(any());
584 
585         // Missing repeatCounter.
586         v = mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0,
587                 new int[]{CustomInputEvent.INPUT_CODE_F1,
588                         CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER});
589         mInputHalService.onHalEvents(List.of(v));
590 
591         verify(mInputListener, never()).onCustomInputEvent(any());
592     }
593 
594     @Test
dispatchesCustomInputEvent_acceptInputCodeHigherThanF10()595     public void dispatchesCustomInputEvent_acceptInputCodeHigherThanF10() {
596         // Custom Input Events may accept input code values outside the
597         // CUSTOM_EVENT_F1 to F10 range.
598         int someInputCodeValueHigherThanF10 = 1000;
599 
600         // Arrange mInputListener to capture incoming CustomInputEvent
601         subscribeListener();
602 
603         List<CustomInputEvent> events = new ArrayList<>();
604         doAnswer(invocation -> {
605             CustomInputEvent event = invocation.getArgument(0);
606             events.add(event);
607             return null;
608         }).when(mInputListener).onCustomInputEvent(any());
609 
610         // Arrange
611         int repeatCounter = 1;
612         HalPropValue customInputPropValue = makeCustomInputPropValue(
613                 someInputCodeValueHigherThanF10, VehicleDisplay.MAIN, repeatCounter);
614 
615         // Act
616         mInputHalService.onHalEvents(List.of(customInputPropValue));
617 
618         // Assert
619         assertThat(events).containsExactly(new CustomInputEvent(
620                 someInputCodeValueHigherThanF10, CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
621                 repeatCounter));
622     }
623 
subscribeListener()624     private void subscribeListener() {
625         mInputHalService.takeProperties(Set.of(HW_KEY_INPUT_CONFIG));
626         assertThat(mInputHalService.isKeyInputSupported()).isTrue();
627 
628         mInputHalService.setInputListener(mInputListener);
629         verify(mVehicleHal).subscribeProperty(mInputHalService, VehicleProperty.HW_KEY_INPUT);
630     }
631 
dispatchSingleEvent(Key action, int code, int actualDisplay, @DisplayTypeEnum int expectedDisplay)632     private KeyEvent dispatchSingleEvent(Key action, int code, int actualDisplay,
633             @DisplayTypeEnum int expectedDisplay) {
634         ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
635         reset(mInputListener);
636         mInputHalService.onHalEvents(
637                 List.of(makeKeyPropValue(action, code, actualDisplay)));
638         verify(mInputListener).onKeyEvent(captor.capture(), eq(expectedDisplay));
639         reset(mInputListener);
640         return captor.getValue();
641     }
642 
dispatchSingleEventWithIndents(int code, int indents, int actualDisplay, @DisplayTypeEnum int expectedDisplay)643     private KeyEvent dispatchSingleEventWithIndents(int code, int indents, int actualDisplay,
644             @DisplayTypeEnum int expectedDisplay) {
645         ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
646         reset(mInputListener);
647         mInputHalService.onHalEvents(
648                 List.of(makeKeyPropValueWithIndents(code, indents, actualDisplay)));
649         verify(mInputListener, times(indents)).onKeyEvent(captor.capture(),
650                 eq(expectedDisplay));
651         reset(mInputListener);
652         return captor.getValue();
653     }
654 
makeKeyPropValue(Key action, int code, @DisplayTypeEnum int targetDisplayType)655     private HalPropValue makeKeyPropValue(Key action, int code,
656             @DisplayTypeEnum int targetDisplayType) {
657         int actionValue = (action == Key.DOWN
658                         ? VehicleHwKeyInputAction.ACTION_DOWN
659                         : VehicleHwKeyInputAction.ACTION_UP);
660         return mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
661                 new int[]{actionValue, code, targetDisplayType});
662     }
663 
makeKeyPropValueWithIndents(int code, int indents, @DisplayTypeEnum int targetDisplayType)664     private HalPropValue makeKeyPropValueWithIndents(int code, int indents,
665             @DisplayTypeEnum int targetDisplayType) {
666         // Only Key.down can have indents.
667         return mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
668                 new int[]{VehicleHwKeyInputAction.ACTION_DOWN, code, targetDisplayType, indents});
669     }
670 
makeRotaryPropValue(int rotaryInputType, int detents, long timestamp, int delayBetweenDetents, @DisplayTypeEnum int targetDisplayType)671     private HalPropValue makeRotaryPropValue(int rotaryInputType, int detents, long timestamp,
672             int delayBetweenDetents, @DisplayTypeEnum int targetDisplayType) {
673         ArrayList<Integer> int32Values = new ArrayList<>();
674         int32Values.add(rotaryInputType);
675         int32Values.add(detents);
676         int32Values.add(targetDisplayType);
677         for (int i = 0; i < Math.abs(detents) - 1; i++) {
678             int32Values.add(delayBetweenDetents);
679         }
680         return mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0, timestamp,
681                 /*status=*/0, toIntArray(int32Values));
682     }
683 
makeCustomInputPropValue(int inputCode, @DisplayTypeEnum int targetDisplayType, int repeatCounter)684     private HalPropValue makeCustomInputPropValue(int inputCode,
685             @DisplayTypeEnum int targetDisplayType, int repeatCounter) {
686         return mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0,
687                 new int[]{inputCode, targetDisplayType, repeatCounter});
688     }
689 }
690