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();
+ }
+}