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 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.ArgumentMatchers.eq;
29 import static org.mockito.Mockito.atLeastOnce;
30 import static org.mockito.Mockito.doAnswer;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.reset;
33 import static org.mockito.Mockito.times;
34 import static org.mockito.Mockito.verify;
35 import static org.mockito.Mockito.when;
36 
37 import static java.util.concurrent.TimeUnit.SECONDS;
38 
39 import android.car.CarOccupantZoneManager;
40 import android.car.input.CarInputManager;
41 import android.car.input.CustomInputEvent;
42 import android.car.input.RotaryEvent;
43 import android.hardware.automotive.vehicle.RotaryInputType;
44 import android.hardware.automotive.vehicle.VehicleDisplay;
45 import android.hardware.automotive.vehicle.VehicleHwKeyInputAction;
46 import android.hardware.automotive.vehicle.VehicleHwMotionButtonStateFlag;
47 import android.hardware.automotive.vehicle.VehicleHwMotionInputAction;
48 import android.hardware.automotive.vehicle.VehicleHwMotionInputSource;
49 import android.hardware.automotive.vehicle.VehicleHwMotionToolType;
50 import android.hardware.automotive.vehicle.VehicleProperty;
51 import android.hardware.automotive.vehicle.VehiclePropertyStatus;
52 import android.view.InputDevice;
53 import android.view.KeyEvent;
54 import android.view.MotionEvent;
55 
56 import androidx.test.filters.RequiresDevice;
57 
58 import com.android.car.hal.test.AidlVehiclePropConfigBuilder;
59 
60 import org.junit.After;
61 import org.junit.Before;
62 import org.junit.Test;
63 import org.junit.runner.RunWith;
64 import org.mockito.ArgumentCaptor;
65 import org.mockito.Mock;
66 import org.mockito.junit.MockitoJUnitRunner;
67 
68 import java.io.PrintWriter;
69 import java.util.ArrayList;
70 import java.util.List;
71 import java.util.Set;
72 import java.util.function.LongSupplier;
73 
74 @RunWith(MockitoJUnitRunner.class)
75 public class InputHalServiceTest {
76     @Mock VehicleHal mVehicleHal;
77     @Mock InputHalService.InputListener mInputListener;
78     @Mock LongSupplier mUptimeSupplier;
79     @Mock PrintWriter mMockPrintWriter;
80 
81     private static final HalPropConfig HW_KEY_INPUT_CONFIG = new AidlHalPropConfig(
82             AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_KEY_INPUT).build());
83     private static final HalPropConfig HW_ROTARY_INPUT_CONFIG = new AidlHalPropConfig(
84             AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_ROTARY_INPUT).build());
85     private static final HalPropConfig HW_CUSTOM_INPUT_CONFIG = new AidlHalPropConfig(
86             AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_CUSTOM_INPUT).build());
87 
88     private final HalPropValueBuilder mPropValueBuilder = new HalPropValueBuilder(/*isAidl=*/true);
89 
90     private enum Key {DOWN, UP}
91 
92     private InputHalService mInputHalService;
93 
94     @Before
setUp()95     public void setUp() {
96         when(mUptimeSupplier.getAsLong()).thenReturn(0L);
97         mInputHalService = new InputHalService(mVehicleHal, mUptimeSupplier);
98         mInputHalService.init();
99     }
100 
101     @After
tearDown()102     public void tearDown() {
103         mInputHalService.release();
104         mInputHalService = null;
105     }
106 
107     @Test
ignoresSetListener_beforeKeyInputSupported()108     public void ignoresSetListener_beforeKeyInputSupported() {
109         assertThat(mInputHalService.isKeyInputSupported()).isFalse();
110 
111         mInputHalService.setInputListener(mInputListener);
112 
113         int anyDisplay = VehicleDisplay.MAIN;
114         mInputHalService.onHalEvents(List.of(makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER,
115                 anyDisplay)));
116         verify(mInputListener, never()).onKeyEvent(any(), anyInt());
117     }
118 
119     @Test
takesKeyInputProperty()120     public void takesKeyInputProperty() {
121         Set<HalPropConfig> offeredProps = Set.of(new AidlHalPropConfig(
122                 AidlVehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build()),
123                 HW_KEY_INPUT_CONFIG,
124                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
125                         VehicleProperty.CURRENT_GEAR).build()));
126 
127         mInputHalService.takeProperties(offeredProps);
128 
129         assertThat(mInputHalService.isKeyInputSupported()).isTrue();
130         assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
131         assertThat(mInputHalService.isCustomInputSupported()).isFalse();
132     }
133 
134     @Test
takesRotaryInputProperty()135     public void takesRotaryInputProperty() {
136         Set<HalPropConfig> offeredProps = Set.of(
137                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
138                         VehicleProperty.ABS_ACTIVE).build()),
139                 HW_ROTARY_INPUT_CONFIG,
140                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
141                         VehicleProperty.CURRENT_GEAR).build()));
142 
143         mInputHalService.takeProperties(offeredProps);
144 
145         assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
146         assertThat(mInputHalService.isKeyInputSupported()).isFalse();
147         assertThat(mInputHalService.isCustomInputSupported()).isFalse();
148     }
149 
150     @Test
takesCustomInputProperty()151     public void takesCustomInputProperty() {
152         Set<HalPropConfig> offeredProps = Set.of(
153                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
154                         VehicleProperty.ABS_ACTIVE).build()),
155                 HW_CUSTOM_INPUT_CONFIG,
156                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
157                         VehicleProperty.CURRENT_GEAR).build()));
158 
159         mInputHalService.takeProperties(offeredProps);
160 
161         assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
162         assertThat(mInputHalService.isKeyInputSupported()).isFalse();
163         assertThat(mInputHalService.isCustomInputSupported()).isTrue();
164     }
165 
166     @Test
takesKeyAndRotaryAndCustomInputProperty()167     public void takesKeyAndRotaryAndCustomInputProperty() {
168         Set<HalPropConfig> offeredProps = Set.of(
169                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
170                         VehicleProperty.ABS_ACTIVE).build()),
171                 HW_KEY_INPUT_CONFIG,
172                 HW_ROTARY_INPUT_CONFIG,
173                 HW_CUSTOM_INPUT_CONFIG,
174                 new AidlHalPropConfig(AidlVehiclePropConfigBuilder.newBuilder(
175                         VehicleProperty.CURRENT_GEAR).build()));
176 
177         mInputHalService.takeProperties(offeredProps);
178 
179         assertThat(mInputHalService.isKeyInputSupported()).isTrue();
180         assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
181         assertThat(mInputHalService.isCustomInputSupported()).isTrue();
182     }
183 
184     @Test
dispatchesInputEvent_single_toListener_mainDisplay()185     public void dispatchesInputEvent_single_toListener_mainDisplay() {
186         subscribeListener();
187 
188         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
189                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
190         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
191         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
192     }
193 
194     @Test
dispatchesInputEvent_single_toListener_clusterDisplay()195     public void dispatchesInputEvent_single_toListener_clusterDisplay() {
196         subscribeListener();
197 
198         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER,
199                 VehicleDisplay.INSTRUMENT_CLUSTER,
200                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
201         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
202         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
203     }
204 
205     @Test
dispatchesInputEvent_multiple_toListener_mainDisplay()206     public void dispatchesInputEvent_multiple_toListener_mainDisplay() {
207         subscribeListener();
208 
209         // KeyEvents get recycled, so we can't just use ArgumentCaptor#getAllValues here.
210         // We need to make a copy of the information we need at the time of the call.
211         List<KeyEvent> events = new ArrayList<>();
212         doAnswer(inv -> {
213             KeyEvent event = inv.getArgument(0);
214             events.add(event.copy());
215             return null;
216         }).when(mInputListener).onKeyEvent(any(), eq(CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
217 
218         mInputHalService.onHalEvents(
219                 List.of(
220                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN),
221                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_MENU, VehicleDisplay.MAIN)));
222 
223         assertThat(events.get(0).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
224         assertThat(events.get(1).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
225 
226         events.forEach(KeyEvent::recycle);
227     }
228 
229     @Test
dispatchesInputEvent_multiple_toListener_clusterDisplay()230     public void dispatchesInputEvent_multiple_toListener_clusterDisplay() {
231         subscribeListener();
232 
233         // KeyEvents get recycled, so we can't just use ArgumentCaptor#getAllValues here.
234         // We need to make a copy of the information we need at the time of the call.
235         List<KeyEvent> events = new ArrayList<>();
236         doAnswer(inv -> {
237             KeyEvent event = inv.getArgument(0);
238             events.add(event.copy());
239             return null;
240         }).when(mInputListener).onKeyEvent(any(),
241                 eq(CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER));
242 
243         mInputHalService.onHalEvents(
244                 List.of(
245                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER,
246                                 VehicleDisplay.INSTRUMENT_CLUSTER),
247                         makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_MENU,
248                                 VehicleDisplay.INSTRUMENT_CLUSTER)));
249 
250         assertThat(events.get(0).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
251         assertThat(events.get(1).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
252 
253         events.forEach(KeyEvent::recycle);
254     }
255 
256     @Test
dispatchesInputEvent_invalidInputEvent()257     public void dispatchesInputEvent_invalidInputEvent() {
258         subscribeListener();
259         HalPropValue v = mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0);
260         // Missing action, code, display_type.
261         mInputHalService.onHalEvents(List.of(v));
262         verify(mInputListener, never()).onKeyEvent(any(),  anyInt());
263 
264         // Missing code, display_type.
265         v = mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
266                 VehicleHwKeyInputAction.ACTION_DOWN);
267         mInputHalService.onHalEvents(List.of(v));
268         verify(mInputListener, never()).onKeyEvent(any(),  anyInt());
269 
270         // Missing display_type.
271         v = mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
272                 new int[]{VehicleHwKeyInputAction.ACTION_DOWN, KeyEvent.KEYCODE_ENTER});
273         mInputHalService.onHalEvents(List.of(v));
274         verify(mInputListener, never()).onKeyEvent(any(),  anyInt());
275     }
276 
277     @Test
handlesRepeatedKeys_anyDisplay()278     public void handlesRepeatedKeys_anyDisplay() {
279         subscribeListener();
280 
281         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
282                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
283         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
284         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
285         assertThat(event.getEventTime()).isEqualTo(0L);
286         assertThat(event.getDownTime()).isEqualTo(0L);
287         assertThat(event.getRepeatCount()).isEqualTo(0);
288 
289         when(mUptimeSupplier.getAsLong()).thenReturn(5L);
290         event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
291                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
292 
293         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
294         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
295         assertThat(event.getEventTime()).isEqualTo(5L);
296         assertThat(event.getDownTime()).isEqualTo(5L);
297         assertThat(event.getRepeatCount()).isEqualTo(1);
298 
299         when(mUptimeSupplier.getAsLong()).thenReturn(10L);
300         event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
301                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
302 
303         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_UP);
304         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
305         assertThat(event.getEventTime()).isEqualTo(10L);
306         assertThat(event.getDownTime()).isEqualTo(5L);
307         assertThat(event.getRepeatCount()).isEqualTo(0);
308 
309         when(mUptimeSupplier.getAsLong()).thenReturn(15L);
310         event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
311                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
312 
313         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
314         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
315         assertThat(event.getEventTime()).isEqualTo(15L);
316         assertThat(event.getDownTime()).isEqualTo(15L);
317         assertThat(event.getRepeatCount()).isEqualTo(0);
318     }
319 
320     /**
321      * Test for handling rotary knob event.
322      */
323     @RequiresDevice
324     @Test
handlesRepeatedKeyWithIndents_anyDisplay()325     public void handlesRepeatedKeyWithIndents_anyDisplay() {
326         subscribeListener();
327         KeyEvent event = dispatchSingleEventWithIndents(KeyEvent.KEYCODE_VOLUME_UP, 5,
328                 VehicleDisplay.MAIN, CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
329         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
330         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
331         assertThat(event.getEventTime()).isEqualTo(0L);
332         assertThat(event.getDownTime()).isEqualTo(0L);
333         assertThat(event.getRepeatCount()).isEqualTo(4);
334 
335         when(mUptimeSupplier.getAsLong()).thenReturn(5L);
336         event = dispatchSingleEventWithIndents(KeyEvent.KEYCODE_VOLUME_UP, 5,
337                 VehicleDisplay.MAIN, CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
338         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
339         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
340         assertThat(event.getEventTime()).isEqualTo(5L);
341         assertThat(event.getDownTime()).isEqualTo(5L);
342         assertThat(event.getRepeatCount()).isEqualTo(9);
343     }
344 
345     @Test
handlesKeyUp_withoutKeyDown_mainDisplay()346     public void handlesKeyUp_withoutKeyDown_mainDisplay() {
347         subscribeListener();
348 
349         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
350         KeyEvent event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
351                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
352 
353         assertThat(event.getEventTime()).isEqualTo(42L);
354         assertThat(event.getDownTime()).isEqualTo(42L);
355         assertThat(event.getRepeatCount()).isEqualTo(0);
356         // event.getDisplayId is not tested since it is assigned by CarInputService
357     }
358 
359     @Test
handlesKeyUp_withoutKeyDown_clusterDisplay()360     public void handlesKeyUp_withoutKeyDown_clusterDisplay() {
361         subscribeListener();
362 
363         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
364         KeyEvent event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER,
365                 VehicleDisplay.INSTRUMENT_CLUSTER,
366                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
367 
368         assertThat(event.getEventTime()).isEqualTo(42L);
369         assertThat(event.getDownTime()).isEqualTo(42L);
370         assertThat(event.getRepeatCount()).isEqualTo(0);
371         // event.getDisplayId is not tested since it is assigned by CarInputService
372     }
373 
374     @Test
separateKeyDownEvents_areIndependent_mainDisplay()375     public void separateKeyDownEvents_areIndependent_mainDisplay() {
376         subscribeListener();
377 
378         when(mUptimeSupplier.getAsLong()).thenReturn(27L);
379         dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.MAIN,
380                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
381 
382         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
383         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_MENU, VehicleDisplay.MAIN,
384                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
385 
386         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
387         assertThat(event.getDownTime()).isEqualTo(42L);
388         assertThat(event.getRepeatCount()).isEqualTo(0);
389     }
390 
391     @Test
separateKeyDownEvents_areIndependent_clusterDisplay()392     public void separateKeyDownEvents_areIndependent_clusterDisplay() {
393         subscribeListener();
394 
395         when(mUptimeSupplier.getAsLong()).thenReturn(27L);
396         dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER, VehicleDisplay.INSTRUMENT_CLUSTER,
397                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
398 
399         when(mUptimeSupplier.getAsLong()).thenReturn(42L);
400         KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_MENU, VehicleDisplay.MAIN,
401                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
402 
403         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
404         assertThat(event.getDownTime()).isEqualTo(42L);
405         assertThat(event.getRepeatCount()).isEqualTo(0);
406         // event.getDisplayid is not tested since it is assigned by CarInputService
407     }
408 
409     @Test
dispatchesRotaryEvent_singleVolumeUp_anyDisplay()410     public void dispatchesRotaryEvent_singleVolumeUp_anyDisplay() {
411         subscribeListener();
412 
413         // Arrange mInputListener to capture incoming RotaryEvent
414         List<RotaryEvent> events = new ArrayList<>();
415         doAnswer(invocation -> {
416             RotaryEvent event = invocation.getArgument(0);
417             events.add(event);
418             return null;
419         }).when(mInputListener).onRotaryEvent(any(), eq(CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
420 
421         // Arrange
422         long timestampNanos = 12_345_678_901L;
423 
424         // Act
425         mInputHalService.onHalEvents(List.of(
426                 makeRotaryPropValue(RotaryInputType.ROTARY_INPUT_TYPE_AUDIO_VOLUME, 1,
427                         timestampNanos, 0, VehicleDisplay.MAIN)));
428 
429         // Assert
430 
431         // Expected Rotary event to have only one value for uptimeMillisForClicks since the input
432         // property was created with one detent only. This value will correspond to the event
433         // startup time. See CarServiceUtils#getUptimeToElapsedTimeDeltaInMillis for more detailed
434         // information on how this value is calculated.
435         assertThat(events).containsExactly(new RotaryEvent(
436                 /* inputType= */ CarInputManager.INPUT_TYPE_ROTARY_VOLUME,
437                 /* clockwise= */ true,
438                 /* uptimeMillisForClicks= */ new long[]{12345L}));
439     }
440 
441     @Test
dispatchesRotaryEvent_multipleNavigatePrevious_anyDisplay()442     public void dispatchesRotaryEvent_multipleNavigatePrevious_anyDisplay() {
443         subscribeListener();
444 
445         // Arrange mInputListener to capture incoming RotaryEvent
446         List<RotaryEvent> events = new ArrayList<>();
447         doAnswer(invocation -> {
448             RotaryEvent event = invocation.getArgument(0);
449             events.add(event);
450             return null;
451         }).when(mInputListener).onRotaryEvent(any(), eq(CarOccupantZoneManager.DISPLAY_TYPE_MAIN));
452 
453         // Arrange
454         long timestampNanos = 12_345_000_000L;
455         int deltaNanos = 2_000_000;
456         int numberOfDetents = 3;
457 
458         // Act
459         mInputHalService.onHalEvents(List.of(
460                 makeRotaryPropValue(RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION,
461                         -numberOfDetents, timestampNanos, deltaNanos, VehicleDisplay.MAIN)));
462 
463         // Assert
464 
465         // Expected Rotary event to have 3 values for uptimeMillisForClicks since the input
466         // property value was created with 3 detents. Each value in uptimeMillisForClicks
467         // represents the calculated deltas (in nanoseconds) between pairs of consecutive detents
468         // up times. See InputHalService#dispatchRotaryInput for more detailed information on how
469         // delta times are calculated.
470         assertThat(events).containsExactly(new RotaryEvent(
471                 /* inputType= */CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
472                 /* clockwise= */ false,
473                 /* uptimeMillisForClicks= */ new long[]{12345L, 12347L, 12349L}));
474     }
475 
476     @Test
dispatchesRotaryEvent_invalidInputEvent()477     public void dispatchesRotaryEvent_invalidInputEvent() {
478         subscribeListener();
479         HalPropValue v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0);
480 
481         // Missing rotaryInputType, detentCount, targetDisplayType.
482         mInputHalService.onHalEvents(List.of(v));
483         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
484 
485         // Missing detentCount, targetDisplayType.
486         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0,
487                 RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION);
488         mInputHalService.onHalEvents(List.of(v));
489         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
490 
491         // Missing targetDisplayType.
492         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0,
493                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, 1});
494         mInputHalService.onHalEvents(List.of(v));
495         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
496 
497         // Add targetDisplayType and set detentCount to 0.
498         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0,
499                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, 0,
500                         VehicleDisplay.MAIN});
501         mInputHalService.onHalEvents(List.of(v));
502         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
503 
504         // Set detentCount to 1.
505         // Add additional unnecessary arguments so that the array size does not match detentCount.
506         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 1,
507                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, 1,
508                         VehicleDisplay.MAIN, 0});
509         mInputHalService.onHalEvents(List.of(v));
510         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
511 
512         // Set invalid detentCount.
513         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 1,
514                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, Integer.MAX_VALUE,
515                         VehicleDisplay.MAIN, 0});
516         mInputHalService.onHalEvents(List.of(v));
517         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
518 
519         // Set invalid detentCount.
520         v = mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 1,
521                 new int[]{RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION, Integer.MIN_VALUE,
522                         VehicleDisplay.MAIN, 0});
523         mInputHalService.onHalEvents(List.of(v));
524         verify(mInputListener, never()).onRotaryEvent(any(),  anyInt());
525     }
526 
527     @Test
dispatchesCustomInputEvent_mainDisplay()528     public void dispatchesCustomInputEvent_mainDisplay() {
529         // Arrange mInputListener to capture incoming CustomInputEvent
530         subscribeListener();
531 
532         List<CustomInputEvent> events = new ArrayList<>();
533         doAnswer(invocation -> {
534             CustomInputEvent event = invocation.getArgument(0);
535             events.add(event);
536             return null;
537         }).when(mInputListener).onCustomInputEvent(any());
538 
539         // Arrange
540         int repeatCounter = 1;
541         HalPropValue customInputPropValue = makeCustomInputPropValue(
542                 CUSTOM_EVENT_F1, VehicleDisplay.MAIN, repeatCounter);
543 
544         // Act
545         mInputHalService.onHalEvents(List.of(customInputPropValue));
546 
547         // Assert
548         assertThat(events).containsExactly(new CustomInputEvent(
549                 CustomInputEvent.INPUT_CODE_F1, CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
550                 repeatCounter));
551     }
552 
553     @Test
dispatchesCustomInputEvent_clusterDisplay()554     public void dispatchesCustomInputEvent_clusterDisplay() {
555         // Arrange mInputListener to capture incoming CustomInputEvent
556         subscribeListener();
557 
558         List<CustomInputEvent> events = new ArrayList<>();
559         doAnswer(invocation -> {
560             CustomInputEvent event = invocation.getArgument(0);
561             events.add(event);
562             return null;
563         }).when(mInputListener).onCustomInputEvent(any());
564 
565         // Arrange
566         int repeatCounter = 1;
567         HalPropValue customInputPropValue = makeCustomInputPropValue(
568                 CUSTOM_EVENT_F1, VehicleDisplay.INSTRUMENT_CLUSTER, repeatCounter);
569 
570         // Act
571         mInputHalService.onHalEvents(List.of(customInputPropValue));
572 
573         // Assert
574         assertThat(events).containsExactly(new CustomInputEvent(
575                 CustomInputEvent.INPUT_CODE_F1,
576                 CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER,
577                 repeatCounter));
578     }
579 
580     @Test
dispatchesCustomInputEvent_InvalidEvent()581     public void dispatchesCustomInputEvent_InvalidEvent() {
582         // Arrange mInputListener to capture incoming CustomInputEvent
583         subscribeListener();
584 
585         HalPropValue v = mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0);
586 
587         // Missing inputCode, targetDisplayType, repeatCounter.
588         mInputHalService.onHalEvents(List.of(v));
589         verify(mInputListener, never()).onCustomInputEvent(any());
590 
591         // Missing targetDisplayType, repeatCounter.
592         v = mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0,
593                 CustomInputEvent.INPUT_CODE_F1);
594         mInputHalService.onHalEvents(List.of(v));
595         verify(mInputListener, never()).onCustomInputEvent(any());
596 
597         // Missing repeatCounter.
598         v = mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0,
599                 new int[]{CustomInputEvent.INPUT_CODE_F1,
600                         CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER});
601         mInputHalService.onHalEvents(List.of(v));
602 
603         verify(mInputListener, never()).onCustomInputEvent(any());
604     }
605 
606     @Test
dispatchesCustomInputEvent_acceptInputCodeHigherThanF10()607     public void dispatchesCustomInputEvent_acceptInputCodeHigherThanF10() {
608         // Custom Input Events may accept input code values outside the
609         // CUSTOM_EVENT_F1 to F10 range.
610         int someInputCodeValueHigherThanF10 = 1000;
611 
612         // Arrange mInputListener to capture incoming CustomInputEvent
613         subscribeListener();
614 
615         List<CustomInputEvent> events = new ArrayList<>();
616         doAnswer(invocation -> {
617             CustomInputEvent event = invocation.getArgument(0);
618             events.add(event);
619             return null;
620         }).when(mInputListener).onCustomInputEvent(any());
621 
622         // Arrange
623         int repeatCounter = 1;
624         HalPropValue customInputPropValue = makeCustomInputPropValue(
625                 someInputCodeValueHigherThanF10, VehicleDisplay.MAIN, repeatCounter);
626 
627         // Act
628         mInputHalService.onHalEvents(List.of(customInputPropValue));
629 
630         // Assert
631         assertThat(events).containsExactly(new CustomInputEvent(
632                 someInputCodeValueHigherThanF10, CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
633                 repeatCounter));
634     }
635 
636     @Test
dispatchesKeyInputEvent_perSeat_anyDisplay()637     public void dispatchesKeyInputEvent_perSeat_anyDisplay() {
638         // Arrange
639         subscribeListener();
640 
641         // Act
642         KeyEvent event = dispatchSinglePerSeatEvent(
643                 /* seat= */ 1,
644                 VehicleDisplay.MAIN,
645                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
646                 KeyEvent.KEYCODE_HOME,
647                 Key.UP,
648                 /* repeatCount= */ 1,
649                 /* downTime= */ SECONDS.toNanos(7),
650                 /* timeStampNanos= */ SECONDS.toNanos(8)
651         );
652 
653         // Assert
654         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_HOME);
655         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_UP);
656         assertThat(event.getRepeatCount()).isEqualTo(1);
657         assertThat(event.getDownTime()).isEqualTo(SECONDS.toMillis(7));
658         assertThat(event.getEventTime()).isEqualTo(SECONDS.toMillis(8));
659     }
660 
661     @Test
dispatchesKeyInputEvent_withSameEventTimeAndDownTime_perSeat_whenKeyDown()662     public void dispatchesKeyInputEvent_withSameEventTimeAndDownTime_perSeat_whenKeyDown() {
663         // Arrange
664         subscribeListener();
665 
666         // Act
667         KeyEvent event = dispatchSinglePerSeatEvent(
668                 /* seat= */ 1,
669                 VehicleDisplay.MAIN,
670                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
671                 KeyEvent.KEYCODE_HOME,
672                 Key.DOWN,
673                 /* repeatCount= */ 1,
674                 /* downTime= */ SECONDS.toNanos(7),
675                 /* timeStampNanos= */ SECONDS.toNanos(8)
676         );
677 
678         // Assert
679         assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_HOME);
680         assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
681         assertThat(event.getRepeatCount()).isEqualTo(1);
682         assertThat(event.getDownTime()).isEqualTo(SECONDS.toMillis(7));
683         assertThat(event.getEventTime()).isEqualTo(SECONDS.toMillis(7));
684     }
685 
686     @Test
dispatchesMotionInputEvent_perSeat_anyDisplay()687     public void dispatchesMotionInputEvent_perSeat_anyDisplay() {
688         // Arrange
689         subscribeListener();
690 
691         // Act
692         MotionEvent event = dispatchSinglePerSeatMotionEvent(
693                 /* seat= */ 1,
694                 VehicleDisplay.MAIN,
695                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
696                 VehicleHwMotionInputSource.SOURCE_TOUCHSCREEN,
697                 VehicleHwMotionInputAction.ACTION_DOWN,
698                 VehicleHwMotionButtonStateFlag.BUTTON_PRIMARY,
699                 /* pointerCount= */ 2,
700                 /* pointerIds= */ new int[] {10, 20},
701                 /* toolTypes= */ new int[] {VehicleHwMotionToolType.TOOL_TYPE_FINGER,
702                         VehicleHwMotionToolType.TOOL_TYPE_MOUSE},
703                 /* xData= */ new float[] {100, 200},
704                 /* yData= */ new float[] {300, 400},
705                 /* pressureData= */ new float[] {1, 2},
706                 /* sizeData= */ new float[] {3, 4},
707                 /* downTimeNanos= */ SECONDS.toNanos(7),
708                 /* timeStampNanos= */ SECONDS.toNanos(8)
709                 );
710 
711         // Assert
712         assertThat(event.getSource()).isEqualTo(InputDevice.SOURCE_TOUCHSCREEN);
713         assertThat(event.getAction()).isEqualTo(MotionEvent.ACTION_DOWN);
714         assertThat(event.getButtonState()).isEqualTo(MotionEvent.BUTTON_PRIMARY);
715         assertThat(event.getPointerCount()).isEqualTo(2);
716         assertThat(event.getPointerId(0)).isEqualTo(10);
717         assertThat(event.getPointerId(1)).isEqualTo(20);
718         assertThat(event.getToolType(0)).isEqualTo(MotionEvent.TOOL_TYPE_FINGER);
719         assertThat(event.getToolType(1)).isEqualTo(MotionEvent.TOOL_TYPE_MOUSE);
720         assertThat(event.getX(0)).isEqualTo(100);
721         assertThat(event.getX(1)).isEqualTo(200);
722         assertThat(event.getY(0)).isEqualTo(300);
723         assertThat(event.getY(1)).isEqualTo(400);
724         assertThat(event.getPressure(0)).isEqualTo(1);
725         assertThat(event.getPressure(1)).isEqualTo(2);
726         assertThat(event.getSize(0)).isEqualTo(3);
727         assertThat(event.getSize(1)).isEqualTo(4);
728         assertThat(event.getDownTime()).isEqualTo(SECONDS.toMillis(7));
729         assertThat(event.getEventTime()).isEqualTo(SECONDS.toMillis(8));
730     }
731 
732     @Test
dump_withNoLastKeyEvent_printsEmpty()733     public void dump_withNoLastKeyEvent_printsEmpty() {
734         // Arrange
735         ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class);
736 
737         // Act
738         mInputHalService.dump(mMockPrintWriter);
739 
740         // Assert
741         verify(mMockPrintWriter, atLeastOnce()).println(stringArgumentCaptor.capture());
742         List<String> strings = stringArgumentCaptor.getAllValues();
743         assertThat(strings.get(5)).startsWith("mLastFewDispatchedV2KeyEvents:");
744     }
745 
746     @Test
dump_containsLastFewKeyEvents()747     public void dump_containsLastFewKeyEvents() {
748         // Arrange
749         subscribeListener();
750         ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class);
751         dispatchMultipleV2KeyEvents(KeyEvent.KEYCODE_HOME, /* times= */ 1);
752         dispatchMultipleV2KeyEvents(KeyEvent.KEYCODE_BACK, /* times= */ 9);
753         dispatchMultipleV2KeyEvents(KeyEvent.KEYCODE_VOLUME_UP, /* times= */ 1);
754 
755         // Act
756         mInputHalService.dump(mMockPrintWriter);
757 
758         // Assert
759         verify(mMockPrintWriter, atLeastOnce()).println(stringArgumentCaptor.capture());
760         List<String> strings = stringArgumentCaptor.getAllValues();
761         int numHomeEvents = 0;
762         int numBackEvents = 0;
763         int numVolUpEvents = 0;
764         assertThat(strings.get(5)).startsWith("mLastFewDispatchedV2KeyEvents:");
765         for (int i = 5; i < strings.size(); i++) {
766             if (strings.get(i).contains("keyCode=KEYCODE_HOME")) {
767                 numHomeEvents++;
768             } else if (strings.get(i).contains("keyCode=KEYCODE_BACK")) {
769                 numBackEvents++;
770             } else if (strings.get(i).contains("keyCode=KEYCODE_VOLUME_UP")) {
771                 numVolUpEvents++;
772             }
773         }
774         assertWithMessage("Number of home events").that(numHomeEvents).isEqualTo(0);
775         assertWithMessage("Number of back events").that(numBackEvents).isEqualTo(9);
776         assertWithMessage("Number of volume up events").that(numVolUpEvents).isEqualTo(1);
777     }
778 
779     @Test
dump_containsLastFewMotionEvents()780     public void dump_containsLastFewMotionEvents() {
781         // Arrange
782         subscribeListener();
783         ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class);
784         dispatchMultipleMotionEvents(VehicleHwMotionInputAction.ACTION_DOWN, /* times= */ 1);
785         dispatchMultipleMotionEvents(VehicleHwMotionInputAction.ACTION_MOVE, /* times= */ 9);
786         dispatchMultipleMotionEvents(VehicleHwMotionInputAction.ACTION_UP, /* times= */ 1);
787 
788         // Act
789         mInputHalService.dump(mMockPrintWriter);
790 
791         // Assert
792         verify(mMockPrintWriter, atLeastOnce()).println(stringArgumentCaptor.capture());
793         List<String> strings = stringArgumentCaptor.getAllValues();
794         int numMoveEvents = 0;
795         int numUpEvents = 0;
796         int numDownEvents = 0;
797         assertThat(strings.get(6)).startsWith("mLastFewDispatchedMotionEvents:");
798         for (int i = 6; i < strings.size(); i++) {
799             if (strings.get(i).contains("action=ACTION_DOWN")) {
800                 numDownEvents++;
801             } else if (strings.get(i).contains("action=ACTION_MOVE")) {
802                 numMoveEvents++;
803             } else if (strings.get(i).contains("action=ACTION_UP")) {
804                 numUpEvents++;
805             }
806         }
807         // Number of down events should be 0 as only 10 recent events are stored.
808         assertWithMessage("Number of down events").that(numDownEvents).isEqualTo(0);
809         assertWithMessage("Number of move events").that(numMoveEvents).isEqualTo(9);
810         assertWithMessage("Number of up events").that(numUpEvents).isEqualTo(1);
811     }
812 
dispatchMultipleMotionEvents(@ehicleHwMotionInputAction int action, int times)813     private void dispatchMultipleMotionEvents(@VehicleHwMotionInputAction int action, int times) {
814         for (int i = 0; i < times; i++) {
815             dispatchSinglePerSeatMotionEvent(
816                     /* seat= */ 1,
817                     VehicleDisplay.MAIN,
818                     CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
819                     VehicleHwMotionInputSource.SOURCE_TOUCHSCREEN,
820                     action,
821                     VehicleHwMotionButtonStateFlag.BUTTON_PRIMARY,
822                     /* pointerCount= */ 2,
823                     /* pointerIds= */ new int[]{10, 20},
824                     /* toolTypes= */ new int[]{VehicleHwMotionToolType.TOOL_TYPE_FINGER,
825                             VehicleHwMotionToolType.TOOL_TYPE_MOUSE},
826                     /* xData= */ new float[]{100, 200},
827                     /* yData= */ new float[]{300, 400},
828                     /* pressureData= */ new float[]{1, 2},
829                     /* sizeData= */ new float[]{3, 4},
830                     /* downTimeNanos= */ SECONDS.toNanos(7),
831                     /* timeStampNanos= */ SECONDS.toNanos(8)
832             );
833         }
834     }
835 
dispatchMultipleV2KeyEvents(int keyCode, int times)836     private void dispatchMultipleV2KeyEvents(int keyCode, int times) {
837         for (int i = 0; i < times; i++) {
838             dispatchSinglePerSeatEvent(
839                     /* seat= */ 1,
840                     VehicleDisplay.MAIN,
841                     CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
842                     keyCode,
843                     Key.DOWN,
844                     /* repeatCount= */ 1,
845                     /* downTime= */ SECONDS.toNanos(7),
846                     /* timeStampNanos= */ SECONDS.toNanos(9)
847             );
848         }
849     }
850 
subscribeListener()851     private void subscribeListener() {
852         mInputHalService.takeProperties(Set.of(HW_KEY_INPUT_CONFIG));
853         assertThat(mInputHalService.isKeyInputSupported()).isTrue();
854 
855         mInputHalService.setInputListener(mInputListener);
856         verify(mVehicleHal).subscribePropertySafe(mInputHalService, VehicleProperty.HW_KEY_INPUT);
857     }
858 
dispatchSingleEvent(Key action, int code, int actualDisplay, @DisplayTypeEnum int expectedDisplay)859     private KeyEvent dispatchSingleEvent(Key action, int code, int actualDisplay,
860             @DisplayTypeEnum int expectedDisplay) {
861         ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
862         reset(mInputListener);
863         mInputHalService.onHalEvents(
864                 List.of(makeKeyPropValue(action, code, actualDisplay)));
865         verify(mInputListener).onKeyEvent(captor.capture(), eq(expectedDisplay));
866         reset(mInputListener);
867         return captor.getValue();
868     }
869 
dispatchSinglePerSeatEvent(int seat, int actualDisplay, @DisplayTypeEnum int expectedDisplay, int code, Key action, int repeatCount, long downTime, long timeStampNanos)870     private KeyEvent dispatchSinglePerSeatEvent(int seat, int actualDisplay,
871             @DisplayTypeEnum int expectedDisplay, int code, Key action, int repeatCount,
872             long downTime, long timeStampNanos) {
873         ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
874         reset(mInputListener);
875 
876         int actionValue = (action == Key.DOWN
877                 ? VehicleHwKeyInputAction.ACTION_DOWN
878                 : VehicleHwKeyInputAction.ACTION_UP);
879         mInputHalService.onHalEvents(
880                 List.of(mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT_V2,
881                         /* areaId= */ seat,
882                         /* timestamp= */ timeStampNanos,
883                         VehiclePropertyStatus.AVAILABLE,
884                         /* int32Values= */ new int[] {actualDisplay, code, actionValue,
885                                 repeatCount},
886                         /* floatValues= */ new float[] {},
887                         /* int64Values= */ new long[] {downTime},
888                         /* stringValue= */ "",
889                         /* byteValues= */ new byte[] {}
890                 )));
891         verify(mInputListener).onKeyEvent(captor.capture(), eq(expectedDisplay),
892                 eq(seat));
893         reset(mInputListener);
894         return captor.getValue();
895     }
896 
dispatchSinglePerSeatMotionEvent(int seat, int actualDisplay, @DisplayTypeEnum int expectedDisplay, int inputSource, int motionActionCode, int buttonState, int pointerCount, int[] pointerIds, int[] toolTypes, float[] xData, float[] yData, float[] pressureData, float[] sizeData, long downTimeNanos, long timeStampNanos)897     private MotionEvent dispatchSinglePerSeatMotionEvent(int seat, int actualDisplay,
898             @DisplayTypeEnum int expectedDisplay, int inputSource, int motionActionCode,
899             int buttonState, int pointerCount, int[] pointerIds, int[] toolTypes, float[] xData,
900             float[] yData, float[] pressureData, float[] sizeData, long downTimeNanos,
901             long timeStampNanos) {
902         ArgumentCaptor<MotionEvent> captor = ArgumentCaptor.forClass(MotionEvent.class);
903         reset(mInputListener);
904 
905         int[] int32Array = new int[5 + 2 * pointerCount];
906         float[] floatArray = new float[4 * pointerCount];
907         int32Array[0] = actualDisplay;
908         int32Array[1] = inputSource;
909         int32Array[2] = motionActionCode;
910         int32Array[3] = buttonState;
911         int32Array[4] = pointerCount;
912         for (int i = 0; i < pointerCount; i++) {
913             int32Array[5 + i] = pointerIds[i];
914             int32Array[5 + pointerCount + i] = toolTypes[i];
915             floatArray[i] = xData[i];
916             floatArray[pointerCount + i] = yData[i];
917             floatArray[2 * pointerCount + i] = pressureData[i];
918             floatArray[3 * pointerCount + i] = sizeData[i];
919         }
920 
921         mInputHalService.onHalEvents(
922                 List.of(mPropValueBuilder.build(VehicleProperty.HW_MOTION_INPUT,
923                         /* areaId = */ seat,
924                         /* timestamp= */ timeStampNanos,
925                         VehiclePropertyStatus.AVAILABLE,
926                         int32Array,
927                         floatArray,
928                         /* int64Values= */ new long[] {downTimeNanos},
929                         /* stringValue= */ "",
930                         /* byteValues= */ new byte[] {}
931                 )));
932         verify(mInputListener).onMotionEvent(captor.capture(), eq(expectedDisplay), eq(seat));
933         reset(mInputListener);
934         return captor.getValue();
935     }
936 
dispatchSingleEventWithIndents(int code, int indents, int actualDisplay, @DisplayTypeEnum int expectedDisplay)937     private KeyEvent dispatchSingleEventWithIndents(int code, int indents, int actualDisplay,
938             @DisplayTypeEnum int expectedDisplay) {
939         ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
940         reset(mInputListener);
941         mInputHalService.onHalEvents(
942                 List.of(makeKeyPropValueWithIndents(code, indents, actualDisplay)));
943         verify(mInputListener, times(indents)).onKeyEvent(captor.capture(),
944                 eq(expectedDisplay));
945         reset(mInputListener);
946         return captor.getValue();
947     }
948 
makeKeyPropValue(Key action, int code, @DisplayTypeEnum int targetDisplayType)949     private HalPropValue makeKeyPropValue(Key action, int code,
950             @DisplayTypeEnum int targetDisplayType) {
951         int actionValue = (action == Key.DOWN
952                         ? VehicleHwKeyInputAction.ACTION_DOWN
953                         : VehicleHwKeyInputAction.ACTION_UP);
954         return mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
955                 new int[]{actionValue, code, targetDisplayType});
956     }
957 
makeKeyPropValueWithIndents(int code, int indents, @DisplayTypeEnum int targetDisplayType)958     private HalPropValue makeKeyPropValueWithIndents(int code, int indents,
959             @DisplayTypeEnum int targetDisplayType) {
960         // Only Key.down can have indents.
961         return mPropValueBuilder.build(VehicleProperty.HW_KEY_INPUT, /* areaId= */ 0,
962                 new int[]{VehicleHwKeyInputAction.ACTION_DOWN, code, targetDisplayType, indents});
963     }
964 
makeRotaryPropValue(int rotaryInputType, int detents, long timestamp, int delayBetweenDetents, @DisplayTypeEnum int targetDisplayType)965     private HalPropValue makeRotaryPropValue(int rotaryInputType, int detents, long timestamp,
966             int delayBetweenDetents, @DisplayTypeEnum int targetDisplayType) {
967         ArrayList<Integer> int32Values = new ArrayList<>();
968         int32Values.add(rotaryInputType);
969         int32Values.add(detents);
970         int32Values.add(targetDisplayType);
971         for (int i = 0; i < Math.abs(detents) - 1; i++) {
972             int32Values.add(delayBetweenDetents);
973         }
974         return mPropValueBuilder.build(VehicleProperty.HW_ROTARY_INPUT, /* areaId= */ 0, timestamp,
975                 /*status=*/0, toIntArray(int32Values));
976     }
977 
makeCustomInputPropValue(int inputCode, @DisplayTypeEnum int targetDisplayType, int repeatCounter)978     private HalPropValue makeCustomInputPropValue(int inputCode,
979             @DisplayTypeEnum int targetDisplayType, int repeatCounter) {
980         return mPropValueBuilder.build(VehicleProperty.HW_CUSTOM_INPUT, /* areaId= */ 0,
981                 new int[]{inputCode, targetDisplayType, repeatCounter});
982     }
983 }
984