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