Merge "Make car service handle rotary VHAL property." into rvc-dev
diff --git a/service/src/com/android/car/hal/InputHalService.java b/service/src/com/android/car/hal/InputHalService.java
index 1c8fb8d..d05ceff 100644
--- a/service/src/com/android/car/hal/InputHalService.java
+++ b/service/src/com/android/car/hal/InputHalService.java
@@ -15,7 +15,10 @@
*/
package com.android.car.hal;
+import static android.hardware.automotive.vehicle.V2_0.RotaryInputType.ROTARY_INPUT_TYPE_AUDIO_VOLUME;
+import static android.hardware.automotive.vehicle.V2_0.RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION;
import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_KEY_INPUT;
+import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_ROTARY_INPUT;
import android.hardware.automotive.vehicle.V2_0.VehicleDisplay;
import android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction;
@@ -35,6 +38,7 @@
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
public class InputHalService extends HalServiceBase {
@@ -42,7 +46,10 @@
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. */
+
+ /**
+ * A function to retrieve the current system uptime in milliseconds - replaceable for testing.
+ */
private final LongSupplier mUptimeSupplier;
public interface InputListener {
@@ -59,10 +66,15 @@
private static final boolean DBG = false;
- @GuardedBy("this")
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
private boolean mKeyInputSupported = false;
- @GuardedBy("this")
+ @GuardedBy("mLock")
+ private boolean mRotaryInputSupported = false;
+
+ @GuardedBy("mLock")
private InputListener mListener;
@GuardedBy("mKeyStates")
@@ -79,18 +91,38 @@
}
public void setInputListener(InputListener listener) {
- synchronized (this) {
- if (!mKeyInputSupported) {
- Log.w(CarLog.TAG_INPUT, "input listener set while key input not supported");
+ boolean keyInputSupported;
+ boolean rotaryInputSupported;
+ synchronized (mLock) {
+ if (!mKeyInputSupported && !mRotaryInputSupported) {
+ Log.w(CarLog.TAG_INPUT,
+ "input listener set while rotary and key input not supported");
return;
}
mListener = listener;
+ keyInputSupported = mKeyInputSupported;
+ rotaryInputSupported = mRotaryInputSupported;
}
- mHal.subscribeProperty(this, HW_KEY_INPUT);
+ if (keyInputSupported) {
+ mHal.subscribeProperty(this, HW_KEY_INPUT);
+ }
+ if (rotaryInputSupported) {
+ mHal.subscribeProperty(this, HW_ROTARY_INPUT);
+ }
}
- public synchronized boolean isKeyInputSupported() {
- return mKeyInputSupported;
+ /** Returns whether {@code HW_KEY_INPUT} is supported. */
+ public boolean isKeyInputSupported() {
+ synchronized (mLock) {
+ return mKeyInputSupported;
+ }
+ }
+
+ /** Returns whether {@code HW_ROTARY_INPUT} is supported. */
+ public boolean isRotaryInputSupported() {
+ synchronized (mLock) {
+ return mRotaryInputSupported;
+ }
}
@Override
@@ -99,9 +131,10 @@
@Override
public void release() {
- synchronized (this) {
+ synchronized (mLock) {
mListener = null;
mKeyInputSupported = false;
+ mRotaryInputSupported = false;
}
}
@@ -109,12 +142,20 @@
public Collection<VehiclePropConfig> takeSupportedProperties(
Collection<VehiclePropConfig> allProperties) {
List<VehiclePropConfig> supported = new LinkedList<>();
- for (VehiclePropConfig p: allProperties) {
- if (p.prop == HW_KEY_INPUT) {
- supported.add(p);
- synchronized (this) {
- mKeyInputSupported = true;
- }
+ for (VehiclePropConfig property : allProperties) {
+ switch (property.prop) {
+ case HW_KEY_INPUT:
+ supported.add(property);
+ synchronized (mLock) {
+ mKeyInputSupported = true;
+ }
+ break;
+ case HW_ROTARY_INPUT:
+ supported.add(property);
+ synchronized (mLock) {
+ mRotaryInputSupported = true;
+ }
+ break;
}
}
return supported;
@@ -123,42 +164,124 @@
@Override
public void onHalEvents(List<VehiclePropValue> values) {
InputListener listener;
- synchronized (this) {
+ synchronized (mLock) {
listener = mListener;
}
if (listener == null) {
Log.w(CarLog.TAG_INPUT, "Input event while listener is null");
return;
}
- for (VehiclePropValue v : values) {
- if (v.prop != HW_KEY_INPUT) {
- Log.e(CarLog.TAG_INPUT, "Wrong event dispatched, prop:0x" +
- Integer.toHexString(v.prop));
- continue;
- }
- int action = (v.value.int32Values.get(0) == VehicleHwKeyInputAction.ACTION_DOWN) ?
- KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
- int code = v.value.int32Values.get(1);
- int display = v.value.int32Values.get(2);
- int indentsCount = v.value.int32Values.size() < 4 ? 1 : v.value.int32Values.get(3);
- if (DBG) {
- Log.i(CarLog.TAG_INPUT, new StringBuilder()
- .append("hal event code:").append(code)
- .append(", action:").append(action)
- .append(", display: ").append(display)
- .append(", number of indents: ").append(indentsCount)
- .toString());
- }
- while (indentsCount > 0) {
- indentsCount--;
- dispatchKeyEvent(listener, action, code, display);
+ for (VehiclePropValue value : values) {
+ switch (value.prop) {
+ case HW_KEY_INPUT:
+ dispatchKeyInput(listener, value);
+ break;
+ case HW_ROTARY_INPUT:
+ dispatchRotaryInput(listener, value);
+ break;
+ default:
+ Log.e(CarLog.TAG_INPUT,
+ "Wrong event dispatched, prop:0x" + Integer.toHexString(value.prop));
+ break;
}
}
}
- private void dispatchKeyEvent(InputListener listener, int action, int code, int display) {
- long eventTime = mUptimeSupplier.getAsLong();
+ private void dispatchKeyInput(InputListener listener, VehiclePropValue value) {
+ int action = (value.value.int32Values.get(0) == VehicleHwKeyInputAction.ACTION_DOWN)
+ ? KeyEvent.ACTION_DOWN
+ : KeyEvent.ACTION_UP;
+ int code = value.value.int32Values.get(1);
+ int display = value.value.int32Values.get(2);
+ int indentsCount = value.value.int32Values.size() < 4 ? 1 : value.value.int32Values.get(3);
+ if (DBG) {
+ Log.i(CarLog.TAG_INPUT, new StringBuilder()
+ .append("hal event code:").append(code)
+ .append(", action:").append(action)
+ .append(", display: ").append(display)
+ .append(", number of indents: ").append(indentsCount)
+ .toString());
+ }
+ while (indentsCount > 0) {
+ indentsCount--;
+ dispatchKeyEvent(listener, action, code, display);
+ }
+ }
+ private void dispatchRotaryInput(InputListener listener, VehiclePropValue value) {
+ int rotaryInputType = value.value.int32Values.get(0);
+ int detentCount = value.value.int32Values.get(1);
+ int display = value.value.int32Values.get(2);
+ int intValuesIndex = 3; // remaining values are time deltas in nanoseconds
+ long timestamp = value.timestamp; // for first detent, uptime nanoseconds
+ if (DBG) {
+ Log.i(CarLog.TAG_INPUT, new StringBuilder()
+ .append("hal rotary input type: ").append(rotaryInputType)
+ .append(", number of detents:").append(detentCount)
+ .append(", display: ").append(display)
+ .toString());
+ }
+
+ int keycode;
+ switch (rotaryInputType) {
+ case ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION:
+ // TODO(depstein): Send rotary events directly to RotaryService.
+ keycode = detentCount > 0
+ ? KeyEvent.KEYCODE_NAVIGATE_NEXT
+ : KeyEvent.KEYCODE_NAVIGATE_PREVIOUS;
+ break;
+ case ROTARY_INPUT_TYPE_AUDIO_VOLUME:
+ keycode = detentCount > 0
+ ? KeyEvent.KEYCODE_VOLUME_UP
+ : KeyEvent.KEYCODE_VOLUME_DOWN;
+ break;
+ default:
+ Log.e(CarLog.TAG_INPUT, "Unknown rotary input type: " + rotaryInputType);
+ return;
+ }
+
+ detentCount = Math.abs(detentCount);
+ if (detentCount - 1 != value.value.int32Values.size() - 3) {
+ Log.e(CarLog.TAG_INPUT,
+ String.format("Prop should include %d time deltas, found %d", detentCount - 1,
+ value.value.int32Values.size() - 3));
+ }
+
+ while (detentCount > 0) {
+ detentCount--;
+ long eventTimeMillis = TimeUnit.NANOSECONDS.toMillis(timestamp);
+ dispatchKeyEvent(listener, KeyEvent.ACTION_DOWN, keycode, display, eventTimeMillis);
+ dispatchKeyEvent(listener, KeyEvent.ACTION_UP, keycode, display, eventTimeMillis);
+ if (intValuesIndex < value.value.int32Values.size()) {
+ timestamp += value.value.int32Values.get(intValuesIndex);
+ intValuesIndex++;
+ }
+ }
+ }
+
+ /**
+ * Dispatches a KeyEvent using {@link #mUptimeSupplier} for the event time.
+ *
+ * @param listener listener to dispatch the event to
+ * @param action action for the KeyEvent
+ * @param code keycode for the KeyEvent
+ * @param display target display the event is associated with
+ */
+ private void dispatchKeyEvent(InputListener listener, int action, int code, int display) {
+ dispatchKeyEvent(listener, action, code, display, mUptimeSupplier.getAsLong());
+ }
+
+ /**
+ * Dispatches a KeyEvent.
+ *
+ * @param listener listener to dispatch the event to
+ * @param action action for the KeyEvent
+ * @param code keycode for the KeyEvent
+ * @param display target display the event is associated with
+ * @param eventTime uptime in milliseconds when the event occurred
+ */
+ private void dispatchKeyEvent(InputListener listener, int action, int code, int display,
+ long eventTime) {
long downTime;
int repeat;
@@ -207,6 +330,6 @@
public void dump(PrintWriter writer) {
writer.println("*Input HAL*");
writer.println("mKeyInputSupported:" + mKeyInputSupported);
+ writer.println("mRotaryInputSupported:" + mRotaryInputSupported);
}
-
}
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
index 3d6bcc3..8333a21 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/InputHalServiceTest.java
@@ -27,6 +27,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.hardware.automotive.vehicle.V2_0.RotaryInputType;
import android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction;
import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
@@ -62,6 +63,8 @@
private static final VehiclePropConfig HW_KEY_INPUT_CONFIG =
VehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_KEY_INPUT).build();
+ private static final VehiclePropConfig HW_ROTARY_INPUT_CONFIG =
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.HW_ROTARY_INPUT).build();
private static final int DISPLAY = 42;
private enum Key { DOWN, UP }
@@ -104,6 +107,38 @@
assertThat(takenProps).containsExactly(HW_KEY_INPUT_CONFIG);
assertThat(mInputHalService.isKeyInputSupported()).isTrue();
+ assertThat(mInputHalService.isRotaryInputSupported()).isFalse();
+ }
+
+ @Test
+ public void takesRotaryInputProperty() {
+ Set<VehiclePropConfig> offeredProps = ImmutableSet.of(
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build(),
+ HW_ROTARY_INPUT_CONFIG,
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.CURRENT_GEAR).build());
+
+ Collection<VehiclePropConfig> takenProps =
+ mInputHalService.takeSupportedProperties(offeredProps);
+
+ assertThat(takenProps).containsExactly(HW_ROTARY_INPUT_CONFIG);
+ assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
+ assertThat(mInputHalService.isKeyInputSupported()).isFalse();
+ }
+
+ @Test
+ public void takesKeyAndRotaryInputProperty() {
+ Set<VehiclePropConfig> offeredProps = ImmutableSet.of(
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.ABS_ACTIVE).build(),
+ HW_KEY_INPUT_CONFIG,
+ HW_ROTARY_INPUT_CONFIG,
+ VehiclePropConfigBuilder.newBuilder(VehicleProperty.CURRENT_GEAR).build());
+
+ Collection<VehiclePropConfig> takenProps =
+ mInputHalService.takeSupportedProperties(offeredProps);
+
+ assertThat(takenProps).containsExactly(HW_KEY_INPUT_CONFIG, HW_ROTARY_INPUT_CONFIG);
+ assertThat(mInputHalService.isKeyInputSupported()).isTrue();
+ assertThat(mInputHalService.isRotaryInputSupported()).isTrue();
}
@Test
@@ -228,6 +263,72 @@
assertThat(event.getRepeatCount()).isEqualTo(0);
}
+ @Test
+ public void dispatchesRotaryEvent_singleVolumeUp_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));
+
+ long timestampNanos = 12345678901L;
+ mInputHalService.onHalEvents(ImmutableList.of(
+ makeRotaryPropValue(RotaryInputType.ROTARY_INPUT_TYPE_AUDIO_VOLUME, 1,
+ timestampNanos, 0)));
+
+ long timestampMillis = timestampNanos / 1000000;
+ KeyEvent downEvent = events.get(0);
+ assertThat(downEvent.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
+ assertThat(downEvent.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+ assertThat(downEvent.getEventTime()).isEqualTo(timestampMillis);
+ KeyEvent upEvent = events.get(1);
+ assertThat(upEvent.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_VOLUME_UP);
+ assertThat(upEvent.getAction()).isEqualTo(KeyEvent.ACTION_UP);
+ assertThat(upEvent.getEventTime()).isEqualTo(timestampMillis);
+
+ events.forEach(KeyEvent::recycle);
+ }
+
+ @Test
+ public void dispatchesRotaryEvent_multipleNavigatePrevious_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));
+
+ long timestampNanos = 12345678901L;
+ int deltaNanos = 876543210;
+ int numberOfDetents = 3;
+ mInputHalService.onHalEvents(ImmutableList.of(
+ makeRotaryPropValue(RotaryInputType.ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION,
+ -numberOfDetents, timestampNanos, deltaNanos)));
+
+ for (int i = 0; i < numberOfDetents; i++) {
+ long timestampMillis = (timestampNanos + i * (long) deltaNanos) / 1000000;
+ KeyEvent downEvent = events.get(i * 2);
+ assertThat(downEvent.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_NAVIGATE_PREVIOUS);
+ assertThat(downEvent.getAction()).isEqualTo(KeyEvent.ACTION_DOWN);
+ assertThat(downEvent.getEventTime()).isEqualTo(timestampMillis);
+ KeyEvent upEvent = events.get(i * 2 + 1);
+ assertThat(upEvent.getKeyCode()).isEqualTo(KeyEvent.KEYCODE_NAVIGATE_PREVIOUS);
+ assertThat(upEvent.getAction()).isEqualTo(KeyEvent.ACTION_UP);
+ assertThat(upEvent.getEventTime()).isEqualTo(timestampMillis);
+ }
+
+ events.forEach(KeyEvent::recycle);
+ }
+
private void subscribeListener() {
mInputHalService.takeSupportedProperties(ImmutableSet.of(HW_KEY_INPUT_CONFIG));
assertThat(mInputHalService.isKeyInputSupported()).isTrue();
@@ -278,4 +379,18 @@
reset(mInputListener);
return captor.getValue();
}
+
+ private VehiclePropValue makeRotaryPropValue(int rotaryInputType, int detents, long timestamp,
+ int delayBetweenDetents) {
+ VehiclePropValue v = new VehiclePropValue();
+ v.prop = VehicleProperty.HW_ROTARY_INPUT;
+ v.value.int32Values.add(rotaryInputType);
+ v.value.int32Values.add(detents);
+ v.value.int32Values.add(DISPLAY);
+ for (int i = 0; i < Math.abs(detents) - 1; i++) {
+ v.value.int32Values.add(delayBetweenDetents);
+ }
+ v.timestamp = timestamp;
+ return v;
+ }
}
\ No newline at end of file