Populate repeat field for KeyEvents from VHAL.

This allows consumers to tell whether an ACTION_DOWN KeyEvent is an
initial keypress, or a synthetic event sent when a key is held for an
extended period of time.

This also adds unit tests for all of InputHalService.

Test: atest CarServiceUnitTest
Bug: 33253121
Change-Id: Id397b35506b22c417fddec79fd3797f85ad94078
diff --git a/service/src/com/android/car/hal/InputHalService.java b/service/src/com/android/car/hal/InputHalService.java
index c5174dc..f0993eb 100644
--- a/service/src/com/android/car/hal/InputHalService.java
+++ b/service/src/com/android/car/hal/InputHalService.java
@@ -23,35 +23,59 @@
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.os.SystemClock;
 import android.util.Log;
-import android.util.SparseLongArray;
+import android.util.SparseArray;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 
 import com.android.car.CarLog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.function.LongSupplier;
 
 public class InputHalService extends HalServiceBase {
 
     public static final int DISPLAY_MAIN = VehicleDisplay.MAIN;
     public static final int DISPLAY_INSTRUMENT_CLUSTER = VehicleDisplay.INSTRUMENT_CLUSTER;
     private final VehicleHal mHal;
+    /** A function to retrieve the current system uptime - replaceable for testing. */
+    private final LongSupplier mUptimeSupplier;
 
     public interface InputListener {
         void onKeyEvent(KeyEvent event, int targetDisplay);
     }
 
+    /** The current press state of a key. */
+    private static class KeyState {
+        /** The timestamp (uptimeMillis) of the last ACTION_DOWN event for this key. */
+        public long mLastKeyDownTimestamp = -1;
+        /** The number of ACTION_DOWN events that have been sent for this keypress. */
+        public int mRepeatCount = 0;
+    }
+
     private static final boolean DBG = false;
 
+    @GuardedBy("this")
     private boolean mKeyInputSupported = false;
+
+    @GuardedBy("this")
     private InputListener mListener;
-    private final SparseLongArray mKeyDownTimes = new SparseLongArray();
+
+    @GuardedBy("mKeyStates")
+    private final SparseArray<KeyState> mKeyStates = new SparseArray<>();
 
     public InputHalService(VehicleHal hal) {
+        this(hal, SystemClock::uptimeMillis);
+    }
+
+    @VisibleForTesting
+    InputHalService(VehicleHal hal, LongSupplier uptimeSupplier) {
         mHal = hal;
+        mUptimeSupplier = uptimeSupplier;
     }
 
     public void setInputListener(InputListener listener) {
@@ -126,23 +150,43 @@
     }
 
     private void dispatchKeyEvent(InputListener listener, int action, int code, int display) {
-        long eventTime = SystemClock.uptimeMillis();
+        long eventTime = mUptimeSupplier.getAsLong();
 
-        if (action == KeyEvent.ACTION_DOWN) {
-            mKeyDownTimes.put(code, eventTime);
+        long downTime;
+        int repeat;
+
+        synchronized (mKeyStates) {
+            KeyState state = mKeyStates.get(code);
+            if (state == null) {
+                state = new KeyState();
+                mKeyStates.put(code, state);
+            }
+
+            if (action == KeyEvent.ACTION_DOWN) {
+                downTime = eventTime;
+                repeat = state.mRepeatCount++;
+                state.mLastKeyDownTimestamp = eventTime;
+            } else {
+                // Handle key up events without any matching down event by setting the down time to
+                // the event time. This shouldn't happen in practice - keys should be pressed
+                // before they can be released! - but this protects us against HAL weirdness.
+                downTime =
+                        (state.mLastKeyDownTimestamp == -1)
+                                ? eventTime
+                                : state.mLastKeyDownTimestamp;
+                repeat = 0;
+                state.mRepeatCount = 0;
+            }
         }
 
-        long downTime = action == KeyEvent.ACTION_UP
-                ? mKeyDownTimes.get(code, eventTime) : eventTime;
-
         KeyEvent event = KeyEvent.obtain(
                 downTime,
                 eventTime,
                 action,
                 code,
-                0 /* repeat */,
+                repeat,
                 0 /* meta state */,
-                0 /* deviceId*/,
+                0 /* deviceId */,
                 0 /* scancode */,
                 0 /* flags */,
                 InputDevice.SOURCE_CLASS_BUTTON,
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
new file mode 100644
index 0000000..32f8e1d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.hal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.view.KeyEvent;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.car.vehiclehal.test.VehiclePropConfigBuilder;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.LongSupplier;
+
+@RunWith(AndroidJUnit4.class)
+public class InputHalServiceTest {
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock VehicleHal mVehicleHal;
+    @Mock InputHalService.InputListener mInputListener;
+    @Mock LongSupplier mUptimeSupplier;
+
+    private static final VehiclePropConfig HW_KEY_INPUT_CONFIG =
+            VehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_KEY_INPUT).build();
+    private static final int DISPLAY = 42;
+
+    private enum Key { DOWN, UP }
+
+    private InputHalService mInputHalService;
+
+    @Before
+    public void setUp() {
+        when(mUptimeSupplier.getAsLong()).thenReturn(0L);
+        mInputHalService = new InputHalService(mVehicleHal, mUptimeSupplier);
+        mInputHalService.init();
+    }
+
+    @After
+    public void tearDown() {
+        mInputHalService.release();
+        mInputHalService = null;
+    }
+
+    @Test
+    public void ignoresSetListener_beforeKeyInputSupported() {
+        assertThat(mInputHalService.isKeyInputSupported()).isFalse();
+
+        mInputHalService.setInputListener(mInputListener);
+
+        mInputHalService.handleHalEvents(
+                ImmutableList.of(makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER)));
+        verify(mInputListener, never()).onKeyEvent(any(), anyInt());
+    }
+
+    @Test
+    public void takesKeyInputProperty() {
+        Set<VehiclePropConfig> offeredProps = ImmutableSet.of(
+                VehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build(),
+                HW_KEY_INPUT_CONFIG,
+                VehiclePropConfigBuilder.newBuilder(VehicleProperty.CURRENT_GEAR).build());
+
+        Collection<VehiclePropConfig> takenProps =
+                mInputHalService.takeSupportedProperties(offeredProps);
+
+        assertThat(takenProps).containsExactly(HW_KEY_INPUT_CONFIG);
+        assertThat(mInputHalService.isKeyInputSupported()).isTrue();
+    }
+
+    @Test
+    public void dispatchesInputEvent_single_toListener() {
+        subscribeListener();
+
+        KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER);
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
+    }
+
+    @Test
+    public void dispatchesInputEvent_multiple_toListener() {
+        subscribeListener();
+
+        // KeyEvents get recycled, so we can't just use ArgumentCaptor#getAllValues here.
+        // We need to make a copy of the information we need at the time of the call.
+        List<KeyEvent> events = new ArrayList<>();
+        doAnswer(inv -> {
+            KeyEvent event = inv.getArgument(0);
+            events.add(event.copy());
+            return null;
+        }).when(mInputListener).onKeyEvent(any(), eq(DISPLAY));
+
+        mInputHalService.handleHalEvents(
+                ImmutableList.of(
+                        makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_ENTER),
+                        makeKeyPropValue(Key.DOWN, KeyEvent.KEYCODE_MENU)));
+
+        assertThat(events.get(0).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
+        assertThat(events.get(1).getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
+
+        events.forEach(KeyEvent::recycle);
+    }
+
+    @Test
+    public void handlesRepeatedKeys() {
+        subscribeListener();
+
+        KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER);
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
+        assertThat(event.getEventTime()).isEqualTo(0L);
+        assertThat(event.getDownTime()).isEqualTo(0L);
+        assertThat(event.getRepeatCount()).isEqualTo(0);
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(5L);
+        event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER);
+
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
+        assertThat(event.getEventTime()).isEqualTo(5L);
+        assertThat(event.getDownTime()).isEqualTo(5L);
+        assertThat(event.getRepeatCount()).isEqualTo(1);
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(10L);
+        event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER);
+
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_UP);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
+        assertThat(event.getEventTime()).isEqualTo(10L);
+        assertThat(event.getDownTime()).isEqualTo(5L);
+        assertThat(event.getRepeatCount()).isEqualTo(0);
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(15L);
+        event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER);
+
+        assertThat(event.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_ENTER);
+        assertThat(event.getEventTime()).isEqualTo(15L);
+        assertThat(event.getDownTime()).isEqualTo(15L);
+        assertThat(event.getRepeatCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void handlesKeyUp_withoutKeyDown() {
+        subscribeListener();
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(42L);
+        KeyEvent event = dispatchSingleEvent(Key.UP, KeyEvent.KEYCODE_ENTER);
+
+        assertThat(event.getEventTime()).isEqualTo(42L);
+        assertThat(event.getDownTime()).isEqualTo(42L);
+        assertThat(event.getRepeatCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void separateKeyDownEvents_areIndependent() {
+        subscribeListener();
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(27L);
+        dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_ENTER);
+
+        when(mUptimeSupplier.getAsLong()).thenReturn(42L);
+        KeyEvent event = dispatchSingleEvent(Key.DOWN, KeyEvent.KEYCODE_MENU);
+
+        assertThat(event.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_MENU);
+        assertThat(event.getDownTime()).isEqualTo(42L);
+        assertThat(event.getRepeatCount()).isEqualTo(0);
+    }
+
+    private void subscribeListener() {
+        mInputHalService.takeSupportedProperties(ImmutableSet.of(HW_KEY_INPUT_CONFIG));
+        assertThat(mInputHalService.isKeyInputSupported()).isTrue();
+
+        mInputHalService.setInputListener(mInputListener);
+        verify(mVehicleHal).subscribeProperty(mInputHalService, VehicleProperty.HW_KEY_INPUT);
+    }
+
+
+    private VehiclePropValue makeKeyPropValue(Key action, int code) {
+        VehiclePropValue v = new VehiclePropValue();
+        v.prop = VehicleProperty.HW_KEY_INPUT;
+        v.value.int32Values.add(
+                (action == Key.DOWN
+                        ? VehicleHwKeyInputAction.ACTION_DOWN
+                        : VehicleHwKeyInputAction.ACTION_UP));
+        v.value.int32Values.add(code);
+        v.value.int32Values.add(DISPLAY);
+        return v;
+    }
+
+    private KeyEvent dispatchSingleEvent(Key action, int code) {
+        ArgumentCaptor<KeyEvent> captor = ArgumentCaptor.forClass(KeyEvent.class);
+        reset(mInputListener);
+        mInputHalService.handleHalEvents(ImmutableList.of(makeKeyPropValue(action, code)));
+        verify(mInputListener).onKeyEvent(captor.capture(), eq(DISPLAY));
+        reset(mInputListener);
+        return captor.getValue();
+    }
+}