Merge "Improve jank monitor debug overlay" into main
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index cbc20dc..26ff430 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -543,7 +543,7 @@
 
             mRunningTrackers.put(cuj, tracker);
             if (mDebugOverlay != null) {
-                mDebugOverlay.onTrackerAdded(cuj);
+                mDebugOverlay.onTrackerAdded(cuj, tracker.mTracker.hashCode());
             }
 
             return tracker;
@@ -578,7 +578,7 @@
             running.mConfig.getHandler().removeCallbacks(running.mTimeoutAction);
             mRunningTrackers.remove(cuj);
             if (mDebugOverlay != null) {
-                mDebugOverlay.onTrackerRemoved(cuj, reason);
+                mDebugOverlay.onTrackerRemoved(cuj, reason, tracker.hashCode());
             }
             return false;
         }
diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
index 009837b..97f8879 100644
--- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
+++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
@@ -41,13 +41,14 @@
 import android.os.Trace;
 import android.util.DisplayMetrics;
 import android.util.Log;
-import android.util.SparseIntArray;
 import android.view.Display;
 import android.view.View;
 import android.view.WindowManager;
 
 import com.android.internal.jank.FrameTracker.Reasons;
 
+import java.util.ArrayList;
+
 /**
  * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window
  * associated with one of the CUJs being tracked. There's no guarantee which window it will
@@ -68,13 +69,14 @@
 class InteractionMonitorDebugOverlay {
     private static final String TAG = "InteractionMonitorDebug";
     private static final int REASON_STILL_RUNNING = -1000;
+    private static final long HIDE_OVERLAY_DELAY = 2000L;
     // Sparse array where the key in the CUJ and the value is the session status, or null if
     // it's currently running
     private final Application mCurrentApplication;
     private final Handler mUiThread;
     private final DebugOverlayView mDebugOverlayView;
     private final WindowManager mWindowManager;
-    private final SparseIntArray mRunningCujs = new SparseIntArray();
+    private final ArrayList<TrackerState> mRunningCujs = new ArrayList<>();
 
     InteractionMonitorDebugOverlay(@NonNull Application currentApplication,
             @NonNull @UiThread Handler uiThread, @ColorInt int bgColor, double yOffset) {
@@ -94,8 +96,7 @@
                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                         | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
-                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
-                PixelFormat.TRANSLUCENT);
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
                 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
 
@@ -116,30 +117,53 @@
         mWindowManager.addView(mDebugOverlayView, lp);
     }
 
+    private final Runnable mHideOverlayRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mRunningCujs.clear();
+            mDebugOverlayView.setVisibility(INVISIBLE);
+        }
+    };
+
     @AnyThread
-    void onTrackerAdded(@Cuj.CujType int addedCuj) {
+    void onTrackerAdded(@Cuj.CujType int addedCuj, int cookie) {
+        mUiThread.removeCallbacks(mHideOverlayRunnable);
         mUiThread.post(() -> {
             String cujName = Cuj.getNameOfCuj(addedCuj);
-            Log.i(TAG, cujName + " started");
-            // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
-            // is still running
-            mRunningCujs.put(addedCuj, REASON_STILL_RUNNING);
+            Log.i(TAG, cujName + " started (cookie=" + cookie + ")");
+            mRunningCujs.add(new TrackerState(addedCuj, cookie));
             mDebugOverlayView.setVisibility(VISIBLE);
             mDebugOverlayView.invalidate();
         });
     }
 
     @AnyThread
-    void onTrackerRemoved(@Cuj.CujType int removedCuj, @Reasons int reason) {
+    void onTrackerRemoved(@Cuj.CujType int removedCuj, @Reasons int reason, int cookie) {
         mUiThread.post(() -> {
-            mRunningCujs.put(removedCuj, reason);
+            TrackerState foundTracker = null;
+            boolean allTrackersEnded = true;
+            for (int i = 0; i < mRunningCujs.size(); i++) {
+                TrackerState tracker = mRunningCujs.get(i);
+                if (tracker.mCuj == removedCuj && tracker.mCookie == cookie) {
+                    foundTracker = tracker;
+                } else {
+                    // If none of the trackers have REASON_STILL_RUNNING status, then
+                    // all CUJs have ended
+                    allTrackersEnded = allTrackersEnded && tracker.mState != REASON_STILL_RUNNING;
+                }
+            }
+
+            if (foundTracker != null) {
+                foundTracker.mState = reason;
+            }
+
             String cujName = Cuj.getNameOfCuj(removedCuj);
-            Log.i(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled"));
-            // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
-            if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
+            Log.i(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled")
+                    + " (cookie=" + cookie + ")");
+
+            if (allTrackersEnded) {
                 Log.i(TAG, "All CUJs ended");
-                mRunningCujs.clear();
-                mDebugOverlayView.setVisibility(INVISIBLE);
+                mUiThread.postDelayed(mHideOverlayRunnable, HIDE_OVERLAY_DELAY);
             }
             mDebugOverlayView.invalidate();
         });
@@ -152,6 +176,21 @@
         });
     }
 
+    @AnyThread
+    private static class TrackerState {
+        final int mCookie;
+        final int mCuj;
+        int mState;
+
+        private TrackerState(int cuj, int cookie) {
+            mCuj = cuj;
+            mCookie = cookie;
+            // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
+            // is still running
+            mState = REASON_STILL_RUNNING;
+        }
+    }
+
     @UiThread
     private class DebugOverlayView extends View {
         private static final String TRACK_NAME = "InteractionJankMonitor";
@@ -164,7 +203,15 @@
         private final float mDensity;
         private final Paint mDebugPaint;
         private final Paint.FontMetrics mDebugFontMetrics;
-        private final String mPackageName;
+        private final String mPackageNameText;
+
+        final int mPadding;
+        final int mPackageNameFontSize;
+        final int mCujFontSize;
+        final float mCujNameTextHeight;
+        final float mCujStatusWidth;
+        final float mPackageNameTextHeight;
+        final float mPackageNameWidth;
 
         private DebugOverlayView(Context context, @ColorInt int bgColor, double yOffset) {
             super(context);
@@ -176,7 +223,14 @@
             mDebugPaint = new Paint();
             mDebugPaint.setAntiAlias(false);
             mDebugFontMetrics = new Paint.FontMetrics();
-            mPackageName = mCurrentApplication.getPackageName();
+            mPackageNameText = "package:" + mCurrentApplication.getPackageName();
+            mPadding = dipToPx(5);
+            mPackageNameFontSize = dipToPx(12);
+            mCujFontSize = dipToPx(18);
+            mCujNameTextHeight = getTextHeight(mCujFontSize);
+            mCujStatusWidth = mCujNameTextHeight * 1.2f;
+            mPackageNameTextHeight = getTextHeight(mPackageNameFontSize);
+            mPackageNameWidth = getWidthOfText(mPackageNameText, mPackageNameFontSize);
         }
 
         private int dipToPx(int dip) {
@@ -189,11 +243,16 @@
             return mDebugFontMetrics.descent - mDebugFontMetrics.ascent;
         }
 
+        private float getWidthOfText(String text, int fontSize) {
+            mDebugPaint.setTextSize(fontSize);
+            return mDebugPaint.measureText(text);
+        }
+
         private float getWidthOfLongestCujName(int cujFontSize) {
             mDebugPaint.setTextSize(cujFontSize);
             float maxLength = 0;
             for (int i = 0; i < mRunningCujs.size(); i++) {
-                String cujName = Cuj.getNameOfCuj(mRunningCujs.keyAt(i));
+                String cujName = Cuj.getNameOfCuj(mRunningCujs.get(i).mCuj);
                 float textLength = mDebugPaint.measureText(cujName);
                 if (textLength > maxLength) {
                     maxLength = textLength;
@@ -211,47 +270,52 @@
             // performance analysis.
             Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0);
 
-            final int padding = dipToPx(5);
             final int h = getHeight();
             final int w = getWidth();
             final int dy = (int) (h * mYOffset);
-            int packageNameFontSize = dipToPx(12);
-            int cujFontSize = dipToPx(18);
-            final float cujNameTextHeight = getTextHeight(cujFontSize);
-            final float packageNameTextHeight = getTextHeight(packageNameFontSize);
-            float maxLength = getWidthOfLongestCujName(cujFontSize);
+
+            float maxLength = Math.max(mPackageNameWidth, getWidthOfLongestCujName(mCujFontSize))
+                    + mCujStatusWidth;
 
             final int dx = (int) ((w - maxLength) / 2f);
             canvas.translate(dx, dy);
             // Draw background rectangle for displaying the text showing the CUJ name
             mDebugPaint.setColor(mBgColor);
-            canvas.drawRect(-padding * 2, // more padding on top so we can draw the package name
-                    -padding, padding * 2 + maxLength,
-                    padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(),
-                    mDebugPaint);
-            mDebugPaint.setTextSize(packageNameFontSize);
+            canvas.drawRect(-mPadding * 2, // more padding on top so we can draw the package name
+                    -mPadding, mPadding * 2 + maxLength, mPadding * 2 + mPackageNameTextHeight
+                            + mCujNameTextHeight * mRunningCujs.size(), mDebugPaint);
+            mDebugPaint.setTextSize(mPackageNameFontSize);
             mDebugPaint.setColor(Color.BLACK);
             mDebugPaint.setStrikeThruText(false);
-            canvas.translate(0, packageNameTextHeight);
-            canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint);
-            mDebugPaint.setTextSize(cujFontSize);
+            canvas.translate(0, mPackageNameTextHeight);
+            canvas.drawText(mPackageNameText, 0, 0, mDebugPaint);
+            mDebugPaint.setTextSize(mCujFontSize);
             // Draw text for CUJ names
             for (int i = 0; i < mRunningCujs.size(); i++) {
-                int status = mRunningCujs.valueAt(i);
-                if (status == REASON_STILL_RUNNING) {
-                    mDebugPaint.setColor(Color.BLACK);
-                    mDebugPaint.setStrikeThruText(false);
-                } else if (status == REASON_END_NORMAL) {
-                    mDebugPaint.setColor(Color.GRAY);
-                    mDebugPaint.setStrikeThruText(false);
-                } else {
-                    // Cancelled, or otherwise ended for a bad reason
-                    mDebugPaint.setColor(Color.RED);
-                    mDebugPaint.setStrikeThruText(true);
-                }
-                String cujName = Cuj.getNameOfCuj(mRunningCujs.keyAt(i));
-                canvas.translate(0, cujNameTextHeight);
-                canvas.drawText(cujName, 0, 0, mDebugPaint);
+                TrackerState tracker = mRunningCujs.get(i);
+                int status = tracker.mState;
+                String statusText = switch (status) {
+                    case REASON_STILL_RUNNING -> {
+                        mDebugPaint.setColor(Color.BLACK);
+                        mDebugPaint.setStrikeThruText(false);
+                        yield "☐"; // BALLOT BOX
+                    }
+                    case REASON_END_NORMAL -> {
+                        mDebugPaint.setColor(Color.GRAY);
+                        mDebugPaint.setStrikeThruText(false);
+                        yield "✅"; // WHITE HEAVY CHECK MARK
+                    }
+                    default -> {
+                        // Cancelled, or otherwise ended for a bad reason
+                        mDebugPaint.setColor(Color.RED);
+                        mDebugPaint.setStrikeThruText(true);
+                        yield "❌"; // CROSS MARK
+                    }
+                };
+                String cujName = Cuj.getNameOfCuj(tracker.mCuj);
+                canvas.translate(0, mCujNameTextHeight);
+                canvas.drawText(statusText, 0, 0, mDebugPaint);
+                canvas.drawText(cujName, mCujStatusWidth, 0, mDebugPaint);
             }
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0);
         }