Draw a trace of the movement of fingers

Drawing a trace on the touchpad visualizer of the movement of the fingers on the touchpad. The a line between two different coordinates of the trace fades away after MAX_TRACE_HISTORY_DURATION_MILLIS time.

Test: atest TouchpadDebugViewTest
Test: Build
Test: Manual testing
Bug: 366476200
Flag: com.android.hardware.input.touchpad_visualizer
Change-Id: I452e2b4202ed89cb8b5c0441888173c40aa226c2
diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
index 67c3621..2eed9ba 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
@@ -27,20 +27,28 @@
 import com.android.server.input.TouchpadHardwareProperties;
 import com.android.server.input.TouchpadHardwareState;
 
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Map;
+
 public class TouchpadVisualizationView extends View {
     private static final String TAG = "TouchpadVizMain";
     private static final boolean DEBUG = true;
     private static final float DEFAULT_RES_X = 47f;
     private static final float DEFAULT_RES_Y = 45f;
+    private static final float MAX_TRACE_HISTORY_DURATION_SECONDS = 1f;
 
     private final TouchpadHardwareProperties mTouchpadHardwareProperties;
     private float mScaleFactor;
 
-    TouchpadHardwareState mLatestHardwareState = new TouchpadHardwareState(0, 0, 0, 0,
-            new TouchpadFingerState[]{});
+    private final ArrayDeque<TouchpadHardwareState> mHardwareStateHistory =
+            new ArrayDeque<TouchpadHardwareState>();
+    private final Map<Integer, TouchpadFingerState> mTempFingerStatesByTrackingId = new HashMap<>();
 
     private final Paint mOvalStrokePaint;
     private final Paint mOvalFillPaint;
+    private final Paint mTracePaint;
+    private final Paint mCenterPointPaint;
     private final RectF mTempOvalRect = new RectF();
 
     public TouchpadVisualizationView(Context context,
@@ -55,6 +63,29 @@
         mOvalFillPaint = new Paint();
         mOvalFillPaint.setAntiAlias(true);
         mOvalFillPaint.setARGB(255, 0, 0, 0);
+        mTracePaint = new Paint();
+        mTracePaint.setAntiAlias(false);
+        mTracePaint.setARGB(255, 0, 0, 255);
+        mTracePaint.setStyle(Paint.Style.STROKE);
+        mTracePaint.setStrokeWidth(2);
+        mCenterPointPaint = new Paint();
+        mCenterPointPaint.setAntiAlias(true);
+        mCenterPointPaint.setARGB(255, 255, 0, 0);
+        mCenterPointPaint.setStrokeWidth(2);
+    }
+
+    private void removeOldPoints() {
+        float latestTimestamp = mHardwareStateHistory.getLast().getTimestamp();
+
+        while (!mHardwareStateHistory.isEmpty()) {
+            TouchpadHardwareState oldestPoint = mHardwareStateHistory.getFirst();
+            float onScreenTime = latestTimestamp - oldestPoint.getTimestamp();
+            if (onScreenTime >= MAX_TRACE_HISTORY_DURATION_SECONDS) {
+                mHardwareStateHistory.removeFirst();
+            } else {
+                break;
+            }
+        }
     }
 
     private void drawOval(Canvas canvas, float x, float y, float major, float minor, float angle) {
@@ -71,19 +102,22 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
+        if (mHardwareStateHistory.isEmpty()) {
+            return;
+        }
+
+        TouchpadHardwareState latestHardwareState = mHardwareStateHistory.getLast();
+
         float maximumPressure = 0;
-        for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) {
+        for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) {
             maximumPressure = Math.max(maximumPressure, touchpadFingerState.getPressure());
         }
 
-        for (TouchpadFingerState touchpadFingerState : mLatestHardwareState.getFingerStates()) {
-            float newX = translateRange(mTouchpadHardwareProperties.getLeft(),
-                    mTouchpadHardwareProperties.getRight(), 0, getWidth(),
-                    touchpadFingerState.getPositionX());
+        // Visualizing fingers as ovals
+        for (TouchpadFingerState touchpadFingerState : latestHardwareState.getFingerStates()) {
+            float newX = translateX(touchpadFingerState.getPositionX());
 
-            float newY = translateRange(mTouchpadHardwareProperties.getTop(),
-                    mTouchpadHardwareProperties.getBottom(), 0, getHeight(),
-                    touchpadFingerState.getPositionY());
+            float newY = translateY(touchpadFingerState.getPositionY());
 
             float newAngle = translateRange(0, mTouchpadHardwareProperties.getOrientationMaximum(),
                     0, 90, touchpadFingerState.getOrientation());
@@ -102,6 +136,28 @@
 
             drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle);
         }
+
+        mTempFingerStatesByTrackingId.clear();
+
+        // Drawing the trace
+        for (TouchpadHardwareState currentHardwareState : mHardwareStateHistory) {
+            for (TouchpadFingerState currentFingerState : currentHardwareState.getFingerStates()) {
+                TouchpadFingerState prevFingerState = mTempFingerStatesByTrackingId.put(
+                        currentFingerState.getTrackingId(), currentFingerState);
+
+                if (prevFingerState == null) {
+                    continue;
+                }
+
+                float currentX = translateX(currentFingerState.getPositionX());
+                float currentY = translateY(currentFingerState.getPositionY());
+                float prevX = translateX(prevFingerState.getPositionX());
+                float prevY = translateY(prevFingerState.getPositionY());
+
+                canvas.drawLine(prevX, prevY, currentX, currentY, mTracePaint);
+                canvas.drawPoint(currentX, currentY, mCenterPointPaint);
+            }
+        }
     }
 
     /**
@@ -114,7 +170,18 @@
             logHardwareState(schs);
         }
 
-        mLatestHardwareState = schs;
+        if (!mHardwareStateHistory.isEmpty()
+                && mHardwareStateHistory.getLast().getFingerCount() == 0
+                && schs.getFingerCount() > 0) {
+            mHardwareStateHistory.clear();
+        }
+
+        mHardwareStateHistory.addLast(schs);
+        removeOldPoints();
+
+        if (DEBUG) {
+            logFingerTrace();
+        }
 
         invalidate();
     }
@@ -128,6 +195,16 @@
         mScaleFactor = scaleFactor;
     }
 
+    private float translateX(float x) {
+        return translateRange(mTouchpadHardwareProperties.getLeft(),
+                mTouchpadHardwareProperties.getRight(), 0, getWidth(), x);
+    }
+
+    private float translateY(float y) {
+        return translateRange(mTouchpadHardwareProperties.getTop(),
+                mTouchpadHardwareProperties.getBottom(), 0, getHeight(), y);
+    }
+
     private float translateRange(float rangeBeforeMin, float rangeBeforeMax,
             float rangeAfterMin, float rangeAfterMax, float value) {
         return rangeAfterMin + (value - rangeBeforeMin) / (rangeBeforeMax - rangeBeforeMin) * (
@@ -154,4 +231,10 @@
         }
     }
 
-}
+    private void logFingerTrace() {
+        Slog.d(TAG, "Trace size= " + mHardwareStateHistory.size());
+        for (TouchpadFingerState tfs : mHardwareStateHistory.getLast().getFingerStates()) {
+            Slog.d(TAG, "ID= " + tfs.getTrackingId());
+        }
+    }
+}
\ No newline at end of file