Merge "Update InputeTestFragment to display keys held down."
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
index 34ea1cf..1ae330f 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
@@ -19,6 +19,7 @@
 import static android.hardware.automotive.vehicle.V2_0.SubscribeFlags.EVENTS_FROM_CAR;
 import static android.hardware.automotive.vehicle.V2_0.VehicleDisplay.INSTRUMENT_CLUSTER;
 import static android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction.ACTION_DOWN;
+import static android.hardware.automotive.vehicle.V2_0.VehicleHwKeyInputAction.ACTION_UP;
 import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.HW_KEY_INPUT;
 
 import android.annotation.Nullable;
@@ -34,6 +35,8 @@
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.text.method.ScrollingMovementMethod;
 import android.util.Log;
@@ -57,8 +60,11 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.function.Function;
 
 /**
  * Test input event handling to system.
@@ -81,6 +87,74 @@
 
     private IVehicle mVehicle;
 
+    // Helper class for counting the number of key down vs. key up events received from a single
+    // source.
+    private class KeyDownCounter {
+        KeyDownCounter(Function<Integer, String> keyCodeToStringFunc) {
+            mKeyCodeToStringFunc = keyCodeToStringFunc;
+        }
+
+        public synchronized void count(int keyCode, boolean keyDown) {
+            Integer count = mKeyCodeToCountMap.get(keyCode);
+            if (count == null) {
+                count = 0;
+            }
+            count += keyDown ? 1 : -1;
+            if (count == 0) {
+                mKeyCodeToCountMap.remove(keyCode);
+            } else {
+                mKeyCodeToCountMap.put(keyCode, count);
+            }
+            mMainHandler.post(mRefreshKeysDownTextViewRunnable);
+        }
+
+        public synchronized String toString() {
+            StringBuilder sb = new StringBuilder();
+            for (Map.Entry<Integer, Integer> entry : mKeyCodeToCountMap.entrySet()) {
+                sb.append(" ").append(mKeyCodeToStringFunc.apply(entry.getKey()));
+                if (entry.getValue() != 1) {
+                    sb.append("x").append(entry.getValue());
+                }
+            }
+            return sb.toString();
+        }
+
+        // Function to translate keycodes into strings for this source.
+        private final Function<Integer, String> mKeyCodeToStringFunc;
+
+        // LinkedHashMap used for deterministic iteration order.
+        private final Map<Integer, Integer> mKeyCodeToCountMap = new LinkedHashMap<>();
+    }
+
+    // KeyDownCounters for events from EventReaderService and VHAL. Note that they use different
+    // key code mappings.
+    private final KeyDownCounter mEventReaderServiceKeyDownCounter =
+            new KeyDownCounter(k -> KeypressEvent.keycodeToString(k));
+    private final KeyDownCounter mVhalKeyDownCounter =
+            new KeyDownCounter(k -> KeyEvent.keyCodeToString(k));
+
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+    private TextView mKeysDownTextView;
+
+    // Runnable posted to mMainHandler by KeyDownCounter to refresh mKeysDownTextView's text.
+    private final Runnable mRefreshKeysDownTextViewRunnable = new Runnable() {
+        @Override
+        public void run() {
+            StringBuilder sb = new StringBuilder("VHAL keys down:");
+            sb.append(mVhalKeyDownCounter.toString());
+
+            if (mEventReaderService == null) {
+                sb.append("\nCould not connect to EventReaderService. Is keventreader running?");
+            } else {
+                sb.append("\nEventReaderService keys down:");
+                sb.append(mEventReaderServiceKeyDownCounter.toString());
+            }
+
+            mKeysDownTextView.setText(sb.toString());
+        }
+    };
+
     private EventReaderService mEventReaderService;
 
     private final IEventCallback.Stub mKeypressEventHandler = new IEventCallback.Stub() {
@@ -94,9 +168,7 @@
         @Override
         public void onEvent(KeypressEvent keypressEvent) throws RemoteException {
             Log.d(TAG, "received event " + keypressEvent);
-            synchronized (mInputEventsList) {
-                mInputEventsList.append(prettyPrint(keypressEvent));
-            }
+            mEventReaderServiceKeyDownCounter.count(keypressEvent.keycode, keypressEvent.isKeydown);
         }
     };
 
@@ -113,8 +185,25 @@
 
         @Override
         public void onPropertyEvent(ArrayList<VehiclePropValue> propValues) throws RemoteException {
-            synchronized (mInputEventsList) {
-                propValues.forEach(vpv -> mInputEventsList.append(prettyPrint(vpv)));
+            for (VehiclePropValue vpv : propValues) {
+                Log.d(TAG, "received event " + prettyPrint(vpv));
+                if (vpv.prop != HW_KEY_INPUT || vpv.value == null || vpv.value.int32Values == null
+                        || vpv.value.int32Values.size() < 2) {
+                    continue;
+                }
+                int keycode = vpv.value.int32Values.get(1);
+                switch (vpv.value.int32Values.get(0)) {
+                    case ACTION_DOWN:
+                        mVhalKeyDownCounter.count(keycode, true);
+                        break;
+                    case ACTION_UP:
+                        mVhalKeyDownCounter.count(keycode, false);
+                        break;
+                    default:
+                        Log.e(TAG, "Unrecognized VehicleHwKeyInputAction: "
+                                + vpv.value.int32Values.get(0));
+                        break;
+                }
             }
         }
 
@@ -125,8 +214,6 @@
         public void onPropertySetError(int errorCode, int propId, int areaId) {}
     };
 
-    private TextView mInputEventsList;
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -164,8 +251,9 @@
             @Nullable Bundle savedInstanceState) {
         View view = inflater.inflate(R.layout.input_test, container, false);
 
-        mInputEventsList = view.findViewById(R.id.events_list);
-        mInputEventsList.setMovementMethod(new ScrollingMovementMethod());
+        mKeysDownTextView = view.findViewById(R.id.events_list);
+        mKeysDownTextView.setMovementMethod(new ScrollingMovementMethod());
+        mRefreshKeysDownTextViewRunnable.run();
 
         TextView steeringWheelLabel = new TextView(getActivity() /*context*/);
         steeringWheelLabel.setText(R.string.steering_wheel);
diff --git a/tools/keventreader/client/src/com/android/car/keventreader/KeypressEvent.java b/tools/keventreader/client/src/com/android/car/keventreader/KeypressEvent.java
index 34aa992..8f74011 100644
--- a/tools/keventreader/client/src/com/android/car/keventreader/KeypressEvent.java
+++ b/tools/keventreader/client/src/com/android/car/keventreader/KeypressEvent.java
@@ -17,6 +17,7 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+
 import java.util.HashMap;
 import java.util.Map;
 
@@ -628,6 +629,17 @@
     }
 
     public String keycodeToString() {
-        return KEYCODE_NAME_MAP.getOrDefault(keycode, Integer.toHexString(keycode));
+        return keycodeToString(keycode);
+    }
+
+    /**
+     * Translates a key code from keventreader into a string.
+     * @param keycode Key code from a keventreader KeypressEvent.
+     * @return String String label corresponding to keycode, if available. If not, String with
+     *     hexidecimal representation of keycode.
+     */
+    public static String keycodeToString(int keycode) {
+        String ret = KEYCODE_NAME_MAP.get(keycode);
+        return ret != null ? ret : "0x" + Integer.toHexString(keycode);
     }
 }