diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 6b399def4d7304281ebf4e0e8b03c164112a1aba..2533e029767900e0ac5ecf7bda48b77a273d9e10 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -117,6 +117,7 @@ import com.android.server.DisplayThread;
 import com.android.server.LocalServices;
 import com.android.server.Watchdog;
 import com.android.server.input.InputManagerInternal.LidSwitchCallback;
+import com.android.server.input.debug.FocusEventDebugView;
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.policy.WindowManagerPolicy;
 
diff --git a/services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java b/services/core/java/com/android/server/input/debug/FocusEventDebugGlobalMonitor.java
similarity index 94%
rename from services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java
rename to services/core/java/com/android/server/input/debug/FocusEventDebugGlobalMonitor.java
index 67c221f7703766feb84994a6a3550da59b980477..2b21e49a4e034f31e710db4a6a65e5dce170524a 100644
--- a/services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java
+++ b/services/core/java/com/android/server/input/debug/FocusEventDebugGlobalMonitor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.input;
+package com.android.server.input.debug;
 
 import android.view.Display;
 import android.view.InputEvent;
@@ -22,6 +22,7 @@ import android.view.InputEventReceiver;
 import android.view.MotionEvent;
 
 import com.android.server.UiThread;
+import com.android.server.input.InputManagerService;
 
 /**
  * Receives input events before they are dispatched and reports them to FocusEventDebugView.
diff --git a/services/core/java/com/android/server/input/FocusEventDebugView.java b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java
similarity index 50%
rename from services/core/java/com/android/server/input/FocusEventDebugView.java
rename to services/core/java/com/android/server/input/debug/FocusEventDebugView.java
index 4b8fabde7d35347cb5b267af2b77f548251063e1..6eec0dee91528c5e8176af0c050e1d8f7d1888d2 100644
--- a/services/core/java/com/android/server/input/FocusEventDebugView.java
+++ b/services/core/java/com/android/server/input/debug/FocusEventDebugView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.input;
+package com.android.server.input.debug;
 
 import static android.util.TypedValue.COMPLEX_UNIT_DIP;
 import static android.util.TypedValue.COMPLEX_UNIT_SP;
@@ -24,11 +24,9 @@ import android.animation.LayoutTransition;
 import android.annotation.AnyThread;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Paint;
 import android.graphics.Typeface;
 import android.util.DisplayMetrics;
 import android.util.Pair;
@@ -40,7 +38,6 @@ import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RoundedCorner;
 import android.view.View;
-import android.view.ViewConfiguration;
 import android.view.WindowInsets;
 import android.view.animation.AccelerateInterpolator;
 import android.widget.HorizontalScrollView;
@@ -50,19 +47,17 @@ import android.widget.TextView;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.input.InputManagerService;
 
 import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
 /**
  *  Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on
  *  the screen.
  */
-class FocusEventDebugView extends RelativeLayout {
+public class FocusEventDebugView extends RelativeLayout {
 
     private static final String TAG = FocusEventDebugView.class.getSimpleName();
 
@@ -112,7 +107,7 @@ class FocusEventDebugView extends RelativeLayout {
         mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, mDm);
     }
 
-    FocusEventDebugView(Context c, InputManagerService service) {
+    public FocusEventDebugView(Context c, InputManagerService service) {
         this(c, service, () -> new RotaryInputValueView(c), () -> new RotaryInputGraphView(c));
     }
 
@@ -149,11 +144,13 @@ class FocusEventDebugView extends RelativeLayout {
         return super.dispatchKeyEvent(event);
     }
 
+    /** Determines whether to show the key presses visualization. */
     @AnyThread
     public void updateShowKeyPresses(boolean enabled) {
         post(() -> handleUpdateShowKeyPresses(enabled));
     }
 
+    /** Determines whether to show the rotary input visualization. */
     @AnyThread
     public void updateShowRotaryInput(boolean enabled) {
         post(() -> handleUpdateShowRotaryInput(enabled));
@@ -358,13 +355,6 @@ class FocusEventDebugView extends RelativeLayout {
         return mRotaryInputValueView != null;
     }
 
-    /**
-     * Converts a dimension in scaled pixel units to integer display pixels.
-     */
-    private static int applyDimensionSp(int dimensionSp, DisplayMetrics dm) {
-        return (int) TypedValue.applyDimension(COMPLEX_UNIT_SP, dimensionSp, dm);
-    }
-
     private static class PressedKeyView extends TextView {
 
         private static final ColorFilter sInvertColors = new ColorMatrixColorFilter(new float[]{
@@ -473,376 +463,4 @@ class FocusEventDebugView extends RelativeLayout {
             invalidate();
         }
     }
-
-    // TODO(b/286086154): move RotaryInputGraphView and RotaryInputValueView to a subpackage.
-
-    /** Draws the most recent rotary input value and indicates whether the source is active. */
-    @VisibleForTesting
-    static class RotaryInputValueView extends TextView {
-
-        private static final int INACTIVE_TEXT_COLOR = 0xffff00ff;
-        private static final int ACTIVE_TEXT_COLOR = 0xff420f28;
-        private static final int TEXT_SIZE_SP = 8;
-        private static final int SIDE_PADDING_SP = 4;
-        /** Determines how long the active status lasts. */
-        private static final int ACTIVE_STATUS_DURATION = 250 /* milliseconds */;
-        private static final ColorFilter ACTIVE_BACKGROUND_FILTER =
-                new ColorMatrixColorFilter(new float[]{
-                        0, 0, 0, 0, 255, // red
-                        0, 0, 0, 0,   0, // green
-                        0, 0, 0, 0, 255, // blue
-                        0, 0, 0, 0, 200  // alpha
-                });
-
-        private final Runnable mUpdateActivityStatusCallback = () -> updateActivityStatus(false);
-        private final float mScaledVerticalScrollFactor;
-
-        @VisibleForTesting
-        RotaryInputValueView(Context c) {
-            super(c);
-
-            DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
-            mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor();
-
-            setText(getFormattedValue(0));
-            setTextColor(INACTIVE_TEXT_COLOR);
-            setTextSize(applyDimensionSp(TEXT_SIZE_SP, dm));
-            setPaddingRelative(applyDimensionSp(SIDE_PADDING_SP, dm), 0,
-                    applyDimensionSp(SIDE_PADDING_SP, dm), 0);
-            setTypeface(null, Typeface.BOLD);
-            setBackgroundResource(R.drawable.focus_event_rotary_input_background);
-        }
-
-        void updateValue(float value) {
-            removeCallbacks(mUpdateActivityStatusCallback);
-
-            setText(getFormattedValue(value * mScaledVerticalScrollFactor));
-
-            updateActivityStatus(true);
-            postDelayed(mUpdateActivityStatusCallback, ACTIVE_STATUS_DURATION);
-        }
-
-        @VisibleForTesting
-        void updateActivityStatus(boolean active) {
-            if (active) {
-                setTextColor(ACTIVE_TEXT_COLOR);
-                getBackground().setColorFilter(ACTIVE_BACKGROUND_FILTER);
-            } else {
-                setTextColor(INACTIVE_TEXT_COLOR);
-                getBackground().clearColorFilter();
-            }
-        }
-
-        private static String getFormattedValue(float value) {
-            return String.format("%s%.1f", value < 0 ? "-" : "+", Math.abs(value));
-        }
-    }
-
-    /**
-     * Shows a graph with the rotary input values as a function of time.
-     * The graph gets reset if no action is received for a certain amount of time.
-     */
-    @VisibleForTesting
-    static class RotaryInputGraphView extends View {
-
-        private static final int FRAME_COLOR = 0xbf741b47;
-        private static final int FRAME_WIDTH_SP = 2;
-        private static final int FRAME_BORDER_GAP_SP = 10;
-        private static final int FRAME_TEXT_SIZE_SP = 10;
-        private static final int FRAME_TEXT_OFFSET_SP = 2;
-        private static final int GRAPH_COLOR = 0xffff00ff;
-        private static final int GRAPH_LINE_WIDTH_SP = 1;
-        private static final int GRAPH_POINT_RADIUS_SP = 4;
-        private static final long MAX_SHOWN_TIME_INTERVAL = TimeUnit.SECONDS.toMillis(5);
-        private static final float DEFAULT_FRAME_CENTER_POSITION = 0;
-        private static final int MAX_GRAPH_VALUES_SIZE = 400;
-        /** Maximum time between values so that they are considered part of the same gesture. */
-        private static final long MAX_GESTURE_TIME = TimeUnit.SECONDS.toMillis(1);
-
-        private final DisplayMetrics mDm;
-        /**
-         * Distance in position units (amount scrolled in display pixels) from the center to the
-         * top/bottom frame lines.
-         */
-        private final float mFrameCenterToBorderDistance;
-        private final float mScaledVerticalScrollFactor;
-        private final Locale mDefaultLocale;
-        private final Paint mFramePaint = new Paint();
-        private final Paint mFrameTextPaint = new Paint();
-        private final Paint mGraphLinePaint = new Paint();
-        private final Paint mGraphPointPaint = new Paint();
-
-        private final CyclicBuffer mGraphValues = new CyclicBuffer(MAX_GRAPH_VALUES_SIZE);
-        /** Position at which graph values are placed at the center of the graph. */
-        private float mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION;
-
-        @VisibleForTesting
-        RotaryInputGraphView(Context c) {
-            super(c);
-
-            mDm = mContext.getResources().getDisplayMetrics();
-            // This makes the center-to-border distance equivalent to the display height, meaning
-            // that the total height of the graph is equivalent to 2x the display height.
-            mFrameCenterToBorderDistance = mDm.heightPixels;
-            mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor();
-            mDefaultLocale = Locale.getDefault();
-
-            mFramePaint.setColor(FRAME_COLOR);
-            mFramePaint.setStrokeWidth(applyDimensionSp(FRAME_WIDTH_SP, mDm));
-
-            mFrameTextPaint.setColor(GRAPH_COLOR);
-            mFrameTextPaint.setTextSize(applyDimensionSp(FRAME_TEXT_SIZE_SP, mDm));
-
-            mGraphLinePaint.setColor(GRAPH_COLOR);
-            mGraphLinePaint.setStrokeWidth(applyDimensionSp(GRAPH_LINE_WIDTH_SP, mDm));
-            mGraphLinePaint.setStrokeCap(Paint.Cap.ROUND);
-            mGraphLinePaint.setStrokeJoin(Paint.Join.ROUND);
-
-            mGraphPointPaint.setColor(GRAPH_COLOR);
-            mGraphPointPaint.setStrokeWidth(applyDimensionSp(GRAPH_POINT_RADIUS_SP, mDm));
-            mGraphPointPaint.setStrokeCap(Paint.Cap.ROUND);
-            mGraphPointPaint.setStrokeJoin(Paint.Join.ROUND);
-        }
-
-        /**
-         * Reads new scroll axis value and updates the list accordingly. Old positions are
-         * kept at the front (what you would get with getFirst), while the recent positions are
-         * kept at the back (what you would get with getLast). Also updates the frame center
-         * position to handle out-of-bounds cases.
-         */
-        void addValue(float scrollAxisValue, long eventTime) {
-            // Remove values that are too old.
-            while (mGraphValues.getSize() > 0
-                    && (eventTime - mGraphValues.getFirst().mTime) > MAX_SHOWN_TIME_INTERVAL) {
-                mGraphValues.removeFirst();
-            }
-
-            // If there are no recent values, reset the frame center.
-            if (mGraphValues.getSize() == 0) {
-                mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION;
-            }
-
-            // Handle new value. We multiply the scroll axis value by the scaled scroll factor to
-            // get the amount of pixels to be scrolled. We also compute the accumulated position
-            // by adding the current value to the last one (if not empty).
-            final float displacement = scrollAxisValue * mScaledVerticalScrollFactor;
-            final float prevPos = (mGraphValues.getSize() == 0 ? 0 : mGraphValues.getLast().mPos);
-            final float pos = prevPos + displacement;
-
-            mGraphValues.add(pos, eventTime);
-
-            // The difference between the distance of the most recent position from the center
-            // frame (pos - mFrameCenterPosition) and the maximum allowed distance from the center
-            // frame (mFrameCenterToBorderDistance).
-            final float verticalDiff = Math.abs(pos - mFrameCenterPosition)
-                    - mFrameCenterToBorderDistance;
-            // If needed, translate frame.
-            if (verticalDiff > 0) {
-                final int sign = pos - mFrameCenterPosition < 0 ? -1 : 1;
-                // Here, we update the center frame position by the exact amount needed for us to
-                // stay within the maximum allowed distance from the center frame.
-                mFrameCenterPosition += sign * verticalDiff;
-            }
-
-            // Redraw canvas.
-            invalidate();
-        }
-
-        @Override
-        protected void onDraw(Canvas canvas) {
-            super.onDraw(canvas);
-
-            // Note: vertical coordinates in Canvas go from top to bottom,
-            // that is bottomY > middleY > topY.
-            final int verticalMargin = applyDimensionSp(FRAME_BORDER_GAP_SP, mDm);
-            final int topY = verticalMargin;
-            final int bottomY = getHeight() - verticalMargin;
-            final int middleY = (topY + bottomY) / 2;
-
-            // Note: horizontal coordinates in Canvas go from left to right,
-            // that is rightX > leftX.
-            final int leftX = 0;
-            final int rightX = getWidth();
-
-            // Draw the frame, which includes 3 lines that show the maximum,
-            // minimum and middle positions of the graph.
-            canvas.drawLine(leftX, topY, rightX, topY, mFramePaint);
-            canvas.drawLine(leftX, middleY, rightX, middleY, mFramePaint);
-            canvas.drawLine(leftX, bottomY, rightX, bottomY, mFramePaint);
-
-            // Draw the position that each frame line corresponds to.
-            final int frameTextOffset = applyDimensionSp(FRAME_TEXT_OFFSET_SP, mDm);
-            canvas.drawText(
-                    String.format(mDefaultLocale, "%.1f",
-                            mFrameCenterPosition + mFrameCenterToBorderDistance),
-                    leftX,
-                    topY - frameTextOffset, mFrameTextPaint
-            );
-            canvas.drawText(
-                    String.format(mDefaultLocale, "%.1f", mFrameCenterPosition),
-                    leftX,
-                    middleY - frameTextOffset, mFrameTextPaint
-            );
-            canvas.drawText(
-                    String.format(mDefaultLocale, "%.1f",
-                            mFrameCenterPosition - mFrameCenterToBorderDistance),
-                    leftX,
-                    bottomY - frameTextOffset, mFrameTextPaint
-            );
-
-            // If there are no graph values to be drawn, stop here.
-            if (mGraphValues.getSize() == 0) {
-                return;
-            }
-
-            // Draw the graph using the times and positions.
-            // We start at the most recent value (which should be drawn at the right) and move
-            // to the older values (which should be drawn to the left of more recent ones). Negative
-            // indices are handled by circuling back to the end of the buffer.
-            final long mostRecentTime = mGraphValues.getLast().mTime;
-            float prevCoordX = 0;
-            float prevCoordY = 0;
-            float prevAge = 0;
-            for (Iterator<GraphValue> iter = mGraphValues.reverseIterator(); iter.hasNext();) {
-                final GraphValue value = iter.next();
-
-                final int age = (int) (mostRecentTime - value.mTime);
-                final float pos = value.mPos;
-
-                // We get the horizontal coordinate in time units from left to right with
-                // (MAX_SHOWN_TIME_INTERVAL - age). Then, we rescale it to match the canvas
-                // units by dividing it by the time-domain length (MAX_SHOWN_TIME_INTERVAL)
-                // and by multiplying it by the canvas length (rightX - leftX). Finally, we
-                // offset the coordinate by adding it to leftX.
-                final float coordX = leftX + ((float) (MAX_SHOWN_TIME_INTERVAL - age)
-                        / MAX_SHOWN_TIME_INTERVAL) * (rightX - leftX);
-
-                // We get the vertical coordinate in position units from middle to top with
-                // (pos - mFrameCenterPosition). Then, we rescale it to match the canvas
-                // units by dividing it by half of the position-domain length
-                // (mFrameCenterToBorderDistance) and by multiplying it by half of the canvas
-                // length (middleY - topY). Finally, we offset the coordinate by subtracting
-                // it from middleY (we can't "add" here because the coordinate grows from top
-                // to bottom).
-                final float coordY = middleY - ((pos - mFrameCenterPosition)
-                        / mFrameCenterToBorderDistance) * (middleY - topY);
-
-                // Draw a point for this value.
-                canvas.drawPoint(coordX, coordY, mGraphPointPaint);
-
-                // If this value is part of the same gesture as the previous one, draw a line
-                // between them. We ignore the first value (with age = 0).
-                if (age != 0 && (age - prevAge) <= MAX_GESTURE_TIME) {
-                    canvas.drawLine(prevCoordX, prevCoordY, coordX, coordY, mGraphLinePaint);
-                }
-
-                prevCoordX = coordX;
-                prevCoordY = coordY;
-                prevAge = age;
-            }
-        }
-
-        @VisibleForTesting
-        float getFrameCenterPosition() {
-            return mFrameCenterPosition;
-        }
-
-        /**
-         * Holds data needed to draw each entry in the graph.
-         */
-        private static class GraphValue {
-            /** Position. */
-            float mPos;
-            /** Time when this value was added. */
-            long mTime;
-
-            GraphValue(float pos, long time) {
-                this.mPos = pos;
-                this.mTime = time;
-            }
-        }
-
-        /**
-         * Holds the graph values as a cyclic buffer. It has a fixed capacity, and it replaces the
-         * old values with new ones to avoid creating new objects.
-         */
-        private static class CyclicBuffer {
-            private final GraphValue[] mValues;
-            private final int mCapacity;
-            private int mSize = 0;
-            private int mLastIndex = 0;
-
-            // The iteration index and counter are here to make it easier to reset them.
-            /** Determines the value currently pointed by the iterator. */
-            private int mIteratorIndex;
-            /** Counts how many values have been iterated through. */
-            private int mIteratorCount;
-
-            /** Used traverse the values in reverse order. */
-            private final Iterator<GraphValue> mReverseIterator = new Iterator<GraphValue>() {
-                @Override
-                public boolean hasNext() {
-                    return mIteratorCount <= mSize;
-                }
-
-                @Override
-                public GraphValue next() {
-                    // Returns the value currently pointed by the iterator and moves the iterator to
-                    // the previous one.
-                    mIteratorCount++;
-                    return mValues[(mIteratorIndex-- + mCapacity) % mCapacity];
-                }
-            };
-
-            CyclicBuffer(int capacity) {
-                mCapacity = capacity;
-                mValues = new GraphValue[capacity];
-            }
-
-            /**
-             * Add new graph value. If there is an existing object, we replace its data with the
-             * new one. With this, we re-use old objects instead of creating new ones.
-             */
-            void add(float pos, long time) {
-                mLastIndex = (mLastIndex + 1) % mCapacity;
-                if (mValues[mLastIndex] == null) {
-                    mValues[mLastIndex] = new GraphValue(pos, time);
-                } else {
-                    final GraphValue oldValue = mValues[mLastIndex];
-                    oldValue.mPos = pos;
-                    oldValue.mTime = time;
-                }
-
-                // If needed, account for new value in the buffer size.
-                if (mSize != mCapacity) {
-                    mSize++;
-                }
-            }
-
-            int getSize() {
-                return mSize;
-            }
-
-            GraphValue getFirst() {
-                final int distanceBetweenLastAndFirst = (mCapacity - mSize) + 1;
-                final int firstIndex = (mLastIndex + distanceBetweenLastAndFirst) % mCapacity;
-                return mValues[firstIndex];
-            }
-
-            GraphValue getLast() {
-                return mValues[mLastIndex];
-            }
-
-            void removeFirst() {
-                mSize--;
-            }
-
-            /** Returns an iterator pointing at the last value. */
-            Iterator<GraphValue> reverseIterator() {
-                mIteratorIndex = mLastIndex;
-                mIteratorCount = 1;
-                return mReverseIterator;
-            }
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/input/debug/RotaryInputGraphView.java b/services/core/java/com/android/server/input/debug/RotaryInputGraphView.java
new file mode 100644
index 0000000000000000000000000000000000000000..95635d925855386c4bd5d9a1f3d596c834e763f4
--- /dev/null
+++ b/services/core/java/com/android/server/input/debug/RotaryInputGraphView.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input.debug;
+
+import static android.util.TypedValue.COMPLEX_UNIT_SP;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Shows a graph with the rotary input values as a function of time.
+ * The graph gets reset if no action is received for a certain amount of time.
+ */
+public class RotaryInputGraphView extends View {
+
+    private static final int FRAME_COLOR = 0xbf741b47;
+    private static final int FRAME_WIDTH_SP = 2;
+    private static final int FRAME_BORDER_GAP_SP = 10;
+    private static final int FRAME_TEXT_SIZE_SP = 10;
+    private static final int FRAME_TEXT_OFFSET_SP = 2;
+    private static final int GRAPH_COLOR = 0xffff00ff;
+    private static final int GRAPH_LINE_WIDTH_SP = 1;
+    private static final int GRAPH_POINT_RADIUS_SP = 4;
+    private static final long MAX_SHOWN_TIME_INTERVAL = TimeUnit.SECONDS.toMillis(5);
+    private static final float DEFAULT_FRAME_CENTER_POSITION = 0;
+    private static final int MAX_GRAPH_VALUES_SIZE = 400;
+    /** Maximum time between values so that they are considered part of the same gesture. */
+    private static final long MAX_GESTURE_TIME = TimeUnit.SECONDS.toMillis(1);
+
+    private final DisplayMetrics mDm;
+    /**
+     * Distance in position units (amount scrolled in display pixels) from the center to the
+     * top/bottom frame lines.
+     */
+    private final float mFrameCenterToBorderDistance;
+    private final float mScaledVerticalScrollFactor;
+    private final Locale mDefaultLocale = Locale.getDefault();
+    private final Paint mFramePaint = new Paint();
+    private final Paint mFrameTextPaint = new Paint();
+    private final Paint mGraphLinePaint = new Paint();
+    private final Paint mGraphPointPaint = new Paint();
+
+    private final CyclicBuffer mGraphValues = new CyclicBuffer(MAX_GRAPH_VALUES_SIZE);
+    /** Position at which graph values are placed at the center of the graph. */
+    private float mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION;
+
+    public RotaryInputGraphView(Context c) {
+        super(c);
+
+        mDm = mContext.getResources().getDisplayMetrics();
+        // This makes the center-to-border distance equivalent to the display height, meaning
+        // that the total height of the graph is equivalent to 2x the display height.
+        mFrameCenterToBorderDistance = mDm.heightPixels;
+        mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor();
+
+        mFramePaint.setColor(FRAME_COLOR);
+        mFramePaint.setStrokeWidth(applyDimensionSp(FRAME_WIDTH_SP, mDm));
+
+        mFrameTextPaint.setColor(GRAPH_COLOR);
+        mFrameTextPaint.setTextSize(applyDimensionSp(FRAME_TEXT_SIZE_SP, mDm));
+
+        mGraphLinePaint.setColor(GRAPH_COLOR);
+        mGraphLinePaint.setStrokeWidth(applyDimensionSp(GRAPH_LINE_WIDTH_SP, mDm));
+        mGraphLinePaint.setStrokeCap(Paint.Cap.ROUND);
+        mGraphLinePaint.setStrokeJoin(Paint.Join.ROUND);
+
+        mGraphPointPaint.setColor(GRAPH_COLOR);
+        mGraphPointPaint.setStrokeWidth(applyDimensionSp(GRAPH_POINT_RADIUS_SP, mDm));
+        mGraphPointPaint.setStrokeCap(Paint.Cap.ROUND);
+        mGraphPointPaint.setStrokeJoin(Paint.Join.ROUND);
+    }
+
+    /**
+     * Reads new scroll axis value and updates the list accordingly. Old positions are
+     * kept at the front (what you would get with getFirst), while the recent positions are
+     * kept at the back (what you would get with getLast). Also updates the frame center
+     * position to handle out-of-bounds cases.
+     */
+    public void addValue(float scrollAxisValue, long eventTime) {
+        // Remove values that are too old.
+        while (mGraphValues.getSize() > 0
+                && (eventTime - mGraphValues.getFirst().mTime) > MAX_SHOWN_TIME_INTERVAL) {
+            mGraphValues.removeFirst();
+        }
+
+        // If there are no recent values, reset the frame center.
+        if (mGraphValues.getSize() == 0) {
+            mFrameCenterPosition = DEFAULT_FRAME_CENTER_POSITION;
+        }
+
+        // Handle new value. We multiply the scroll axis value by the scaled scroll factor to
+        // get the amount of pixels to be scrolled. We also compute the accumulated position
+        // by adding the current value to the last one (if not empty).
+        final float displacement = scrollAxisValue * mScaledVerticalScrollFactor;
+        final float prevPos = (mGraphValues.getSize() == 0 ? 0 : mGraphValues.getLast().mPos);
+        final float pos = prevPos + displacement;
+
+        mGraphValues.add(pos, eventTime);
+
+        // The difference between the distance of the most recent position from the center
+        // frame (pos - mFrameCenterPosition) and the maximum allowed distance from the center
+        // frame (mFrameCenterToBorderDistance).
+        final float verticalDiff = Math.abs(pos - mFrameCenterPosition)
+                - mFrameCenterToBorderDistance;
+        // If needed, translate frame.
+        if (verticalDiff > 0) {
+            final int sign = pos - mFrameCenterPosition < 0 ? -1 : 1;
+            // Here, we update the center frame position by the exact amount needed for us to
+            // stay within the maximum allowed distance from the center frame.
+            mFrameCenterPosition += sign * verticalDiff;
+        }
+
+        // Redraw canvas.
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Note: vertical coordinates in Canvas go from top to bottom,
+        // that is bottomY > middleY > topY.
+        final int verticalMargin = applyDimensionSp(FRAME_BORDER_GAP_SP, mDm);
+        final int topY = verticalMargin;
+        final int bottomY = getHeight() - verticalMargin;
+        final int middleY = (topY + bottomY) / 2;
+
+        // Note: horizontal coordinates in Canvas go from left to right,
+        // that is rightX > leftX.
+        final int leftX = 0;
+        final int rightX = getWidth();
+
+        // Draw the frame, which includes 3 lines that show the maximum,
+        // minimum and middle positions of the graph.
+        canvas.drawLine(leftX, topY, rightX, topY, mFramePaint);
+        canvas.drawLine(leftX, middleY, rightX, middleY, mFramePaint);
+        canvas.drawLine(leftX, bottomY, rightX, bottomY, mFramePaint);
+
+        // Draw the position that each frame line corresponds to.
+        final int frameTextOffset = applyDimensionSp(FRAME_TEXT_OFFSET_SP, mDm);
+        canvas.drawText(
+                String.format(mDefaultLocale, "%.1f",
+                        mFrameCenterPosition + mFrameCenterToBorderDistance),
+                leftX,
+                topY - frameTextOffset, mFrameTextPaint
+        );
+        canvas.drawText(
+                String.format(mDefaultLocale, "%.1f", mFrameCenterPosition),
+                leftX,
+                middleY - frameTextOffset, mFrameTextPaint
+        );
+        canvas.drawText(
+                String.format(mDefaultLocale, "%.1f",
+                        mFrameCenterPosition - mFrameCenterToBorderDistance),
+                leftX,
+                bottomY - frameTextOffset, mFrameTextPaint
+        );
+
+        // If there are no graph values to be drawn, stop here.
+        if (mGraphValues.getSize() == 0) {
+            return;
+        }
+
+        // Draw the graph using the times and positions.
+        // We start at the most recent value (which should be drawn at the right) and move
+        // to the older values (which should be drawn to the left of more recent ones). Negative
+        // indices are handled by circuling back to the end of the buffer.
+        final long mostRecentTime = mGraphValues.getLast().mTime;
+        float prevCoordX = 0;
+        float prevCoordY = 0;
+        float prevAge = 0;
+        for (Iterator<GraphValue> iter = mGraphValues.reverseIterator(); iter.hasNext();) {
+            final GraphValue value = iter.next();
+
+            final int age = (int) (mostRecentTime - value.mTime);
+            final float pos = value.mPos;
+
+            // We get the horizontal coordinate in time units from left to right with
+            // (MAX_SHOWN_TIME_INTERVAL - age). Then, we rescale it to match the canvas
+            // units by dividing it by the time-domain length (MAX_SHOWN_TIME_INTERVAL)
+            // and by multiplying it by the canvas length (rightX - leftX). Finally, we
+            // offset the coordinate by adding it to leftX.
+            final float coordX = leftX + ((float) (MAX_SHOWN_TIME_INTERVAL - age)
+                    / MAX_SHOWN_TIME_INTERVAL) * (rightX - leftX);
+
+            // We get the vertical coordinate in position units from middle to top with
+            // (pos - mFrameCenterPosition). Then, we rescale it to match the canvas
+            // units by dividing it by half of the position-domain length
+            // (mFrameCenterToBorderDistance) and by multiplying it by half of the canvas
+            // length (middleY - topY). Finally, we offset the coordinate by subtracting
+            // it from middleY (we can't "add" here because the coordinate grows from top
+            // to bottom).
+            final float coordY = middleY - ((pos - mFrameCenterPosition)
+                    / mFrameCenterToBorderDistance) * (middleY - topY);
+
+            // Draw a point for this value.
+            canvas.drawPoint(coordX, coordY, mGraphPointPaint);
+
+            // If this value is part of the same gesture as the previous one, draw a line
+            // between them. We ignore the first value (with age = 0).
+            if (age != 0 && (age - prevAge) <= MAX_GESTURE_TIME) {
+                canvas.drawLine(prevCoordX, prevCoordY, coordX, coordY, mGraphLinePaint);
+            }
+
+            prevCoordX = coordX;
+            prevCoordY = coordY;
+            prevAge = age;
+        }
+    }
+
+    public float getFrameCenterPosition() {
+        return mFrameCenterPosition;
+    }
+
+    /**
+     * Converts a dimension in scaled pixel units to integer display pixels.
+     */
+    private static int applyDimensionSp(int dimensionSp, DisplayMetrics dm) {
+        return (int) TypedValue.applyDimension(COMPLEX_UNIT_SP, dimensionSp, dm);
+    }
+
+    /**
+     * Holds data needed to draw each entry in the graph.
+     */
+    private static class GraphValue {
+        /** Position. */
+        float mPos;
+        /** Time when this value was added. */
+        long mTime;
+
+        GraphValue(float pos, long time) {
+            this.mPos = pos;
+            this.mTime = time;
+        }
+    }
+
+    /**
+     * Holds the graph values as a cyclic buffer. It has a fixed capacity, and it replaces the
+     * old values with new ones to avoid creating new objects.
+     */
+    private static class CyclicBuffer {
+        private final GraphValue[] mValues;
+        private final int mCapacity;
+        private int mSize = 0;
+        private int mLastIndex = 0;
+
+        // The iteration index and counter are here to make it easier to reset them.
+        /** Determines the value currently pointed by the iterator. */
+        private int mIteratorIndex;
+        /** Counts how many values have been iterated through. */
+        private int mIteratorCount;
+
+        /** Used traverse the values in reverse order. */
+        private final Iterator<GraphValue> mReverseIterator = new Iterator<GraphValue>() {
+            @Override
+            public boolean hasNext() {
+                return mIteratorCount <= mSize;
+            }
+
+            @Override
+            public GraphValue next() {
+                // Returns the value currently pointed by the iterator and moves the iterator to
+                // the previous one.
+                mIteratorCount++;
+                return mValues[(mIteratorIndex-- + mCapacity) % mCapacity];
+            }
+        };
+
+        CyclicBuffer(int capacity) {
+            mCapacity = capacity;
+            mValues = new GraphValue[capacity];
+        }
+
+        /**
+         * Add new graph value. If there is an existing object, we replace its data with the
+         * new one. With this, we re-use old objects instead of creating new ones.
+         */
+        void add(float pos, long time) {
+            mLastIndex = (mLastIndex + 1) % mCapacity;
+            if (mValues[mLastIndex] == null) {
+                mValues[mLastIndex] = new GraphValue(pos, time);
+            } else {
+                final GraphValue oldValue = mValues[mLastIndex];
+                oldValue.mPos = pos;
+                oldValue.mTime = time;
+            }
+
+            // If needed, account for new value in the buffer size.
+            if (mSize != mCapacity) {
+                mSize++;
+            }
+        }
+
+        int getSize() {
+            return mSize;
+        }
+
+        GraphValue getFirst() {
+            final int distanceBetweenLastAndFirst = (mCapacity - mSize) + 1;
+            final int firstIndex = (mLastIndex + distanceBetweenLastAndFirst) % mCapacity;
+            return mValues[firstIndex];
+        }
+
+        GraphValue getLast() {
+            return mValues[mLastIndex];
+        }
+
+        void removeFirst() {
+            mSize--;
+        }
+
+        /** Returns an iterator pointing at the last value. */
+        Iterator<GraphValue> reverseIterator() {
+            mIteratorIndex = mLastIndex;
+            mIteratorCount = 1;
+            return mReverseIterator;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/input/debug/RotaryInputValueView.java b/services/core/java/com/android/server/input/debug/RotaryInputValueView.java
new file mode 100644
index 0000000000000000000000000000000000000000..9fadac57cef941e7ba324e9e3ea63f2831634acb
--- /dev/null
+++ b/services/core/java/com/android/server/input/debug/RotaryInputValueView.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input.debug;
+
+import static android.util.TypedValue.COMPLEX_UNIT_SP;
+
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Typeface;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.ViewConfiguration;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+import java.util.Locale;
+
+/**
+ * Draws the most recent rotary input value and indicates whether the source is active.
+ */
+public class RotaryInputValueView extends TextView {
+
+    private static final int INACTIVE_TEXT_COLOR = 0xffff00ff;
+    private static final int ACTIVE_TEXT_COLOR = 0xff420f28;
+    private static final int TEXT_SIZE_SP = 8;
+    private static final int SIDE_PADDING_SP = 4;
+    /** Determines how long the active status lasts. */
+    private static final int ACTIVE_STATUS_DURATION = 250 /* milliseconds */;
+    private static final ColorFilter ACTIVE_BACKGROUND_FILTER =
+            new ColorMatrixColorFilter(new float[]{
+                    0, 0, 0, 0, 255, // red
+                    0, 0, 0, 0,   0, // green
+                    0, 0, 0, 0, 255, // blue
+                    0, 0, 0, 0, 200  // alpha
+            });
+
+    private final Runnable mUpdateActivityStatusCallback = () -> updateActivityStatus(false);
+    private final float mScaledVerticalScrollFactor;
+    private final Locale mDefaultLocale = Locale.getDefault();
+
+    public RotaryInputValueView(Context c) {
+        super(c);
+
+        DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
+        mScaledVerticalScrollFactor = ViewConfiguration.get(c).getScaledVerticalScrollFactor();
+
+        setText(getFormattedValue(0));
+        setTextColor(INACTIVE_TEXT_COLOR);
+        setTextSize(applyDimensionSp(TEXT_SIZE_SP, dm));
+        setPaddingRelative(applyDimensionSp(SIDE_PADDING_SP, dm), 0,
+                applyDimensionSp(SIDE_PADDING_SP, dm), 0);
+        setTypeface(null, Typeface.BOLD);
+        setBackgroundResource(R.drawable.focus_event_rotary_input_background);
+    }
+
+    /** Updates the shown text with the formatted value. */
+    public void updateValue(float value) {
+        removeCallbacks(mUpdateActivityStatusCallback);
+
+        setText(getFormattedValue(value * mScaledVerticalScrollFactor));
+
+        updateActivityStatus(true);
+        postDelayed(mUpdateActivityStatusCallback, ACTIVE_STATUS_DURATION);
+    }
+
+    /** Updates whether or not there's active rotary input. */
+    public void updateActivityStatus(boolean active) {
+        if (active) {
+            setTextColor(ACTIVE_TEXT_COLOR);
+            getBackground().setColorFilter(ACTIVE_BACKGROUND_FILTER);
+        } else {
+            setTextColor(INACTIVE_TEXT_COLOR);
+            getBackground().clearColorFilter();
+        }
+    }
+
+    private String getFormattedValue(float value) {
+        return String.format(mDefaultLocale, "%s%.1f", value < 0 ? "-" : "+", Math.abs(value));
+    }
+
+    /**
+     * Converts a dimension in scaled pixel units to integer display pixels.
+     */
+    private static int applyDimensionSp(int dimensionSp, DisplayMetrics dm) {
+        return (int) TypedValue.applyDimension(COMPLEX_UNIT_SP, dimensionSp, dm);
+    }
+}
diff --git a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/FocusEventDebugViewTest.java
similarity index 58%
rename from tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java
rename to tests/Input/src/com/android/server/input/debug/FocusEventDebugViewTest.java
index 1b98887199e3f7bc8757346bebda2e66a07d0758..ae7fb3b29f6ce413c2a338477a72cd09d95e6885 100644
--- a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/FocusEventDebugViewTest.java
@@ -14,15 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.server.input;
+package com.android.server.input.debug;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyFloat;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
@@ -31,11 +32,12 @@ import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.MotionEvent.PointerCoords;
 import android.view.MotionEvent.PointerProperties;
-import android.view.ViewConfiguration;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.input.InputManagerService;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -48,76 +50,50 @@ import org.junit.runner.RunWith;
 public class FocusEventDebugViewTest {
 
     private FocusEventDebugView mFocusEventDebugView;
-    private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView;
-    private FocusEventDebugView.RotaryInputGraphView mRotaryInputGraphView;
-    private float mScaledVerticalScrollFactor;
+    private RotaryInputValueView mRotaryInputValueView;
+    private RotaryInputGraphView mRotaryInputGraphView;
 
     @Before
     public void setUp() throws Exception {
         Context context = InstrumentationRegistry.getContext();
-        mScaledVerticalScrollFactor =
-                ViewConfiguration.get(context).getScaledVerticalScrollFactor();
         InputManagerService mockService = mock(InputManagerService.class);
         when(mockService.monitorInput(anyString(), anyInt()))
                 .thenReturn(InputChannel.openInputChannelPair("FocusEventDebugViewTest")[1]);
 
-        mRotaryInputValueView = new FocusEventDebugView.RotaryInputValueView(context);
-        mRotaryInputGraphView = new FocusEventDebugView.RotaryInputGraphView(context);
+        mRotaryInputValueView = spy(new RotaryInputValueView(context));
+        mRotaryInputGraphView = spy(new RotaryInputGraphView(context));
         mFocusEventDebugView = new FocusEventDebugView(context, mockService,
                 () -> mRotaryInputValueView, () -> mRotaryInputGraphView);
     }
 
     @Test
-    public void startsRotaryInputValueViewWithDefaultValue() {
-        assertEquals("+0.0", mRotaryInputValueView.getText());
-    }
-
-    @Test
-    public void startsRotaryInputGraphViewWithDefaultFrameCenter() {
-        assertEquals(0, mRotaryInputGraphView.getFrameCenterPosition(), 0.01);
-    }
-
-    @Test
-    public void handleRotaryInput_updatesRotaryInputValueViewWithScrollValue() {
-        mFocusEventDebugView.handleUpdateShowRotaryInput(true);
-
-        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(0.5f));
-
-        assertEquals(String.format("+%.1f", 0.5f * mScaledVerticalScrollFactor),
-                mRotaryInputValueView.getText());
-    }
-
-    @Test
-    public void handleRotaryInput_translatesRotaryInputGraphViewWithHighScrollValue() {
+    public void handleRotaryInput_sendsMotionEventWhenEnabled() {
         mFocusEventDebugView.handleUpdateShowRotaryInput(true);
 
-        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(1000f));
+        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(0.5f,  10L));
 
-        assertTrue(mRotaryInputGraphView.getFrameCenterPosition() > 0);
+        verify(mRotaryInputGraphView).addValue(0.5f, 10L);
+        verify(mRotaryInputValueView).updateValue(0.5f);
     }
 
     @Test
-    public void updateActivityStatus_setsAndRemovesColorFilter() {
-        // It should not be active initially.
-        assertNull(mRotaryInputValueView.getBackground().getColorFilter());
+    public void handleRotaryInput_doesNotSendMotionEventWhenDisabled() {
+        mFocusEventDebugView.handleUpdateShowRotaryInput(false);
 
-        mRotaryInputValueView.updateActivityStatus(true);
-        // It should be active after rotary input.
-        assertNotNull(mRotaryInputValueView.getBackground().getColorFilter());
+        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(0.5f, 10L));
 
-        mRotaryInputValueView.updateActivityStatus(false);
-        // It should not be active after waiting for mUpdateActivityStatusCallback.
-        assertNull(mRotaryInputValueView.getBackground().getColorFilter());
+        verify(mRotaryInputGraphView, never()).addValue(anyFloat(), anyLong());
+        verify(mRotaryInputValueView, never()).updateValue(anyFloat());
     }
 
-    private MotionEvent createRotaryMotionEvent(float scrollAxisValue) {
+    private MotionEvent createRotaryMotionEvent(float scrollAxisValue, long eventTime) {
         PointerCoords pointerCoords = new PointerCoords();
         pointerCoords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAxisValue);
         PointerProperties pointerProperties = new PointerProperties();
 
         return MotionEvent.obtain(
                 /* downTime */ 0,
-                /* eventTime */ 0,
+                /* eventTime */ eventTime,
                 /* action */ MotionEvent.ACTION_SCROLL,
                 /* pointerCount */ 1,
                 /* pointerProperties */ new PointerProperties[] {pointerProperties},
diff --git a/tests/Input/src/com/android/server/input/debug/RotaryInputGraphViewTest.java b/tests/Input/src/com/android/server/input/debug/RotaryInputGraphViewTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..af6ece414fd162909c99b09bac7d9dbbfc034274
--- /dev/null
+++ b/tests/Input/src/com/android/server/input/debug/RotaryInputGraphViewTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input.debug;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest RotaryInputGraphViewTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class RotaryInputGraphViewTest {
+
+    private RotaryInputGraphView mRotaryInputGraphView;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+
+        mRotaryInputGraphView = new RotaryInputGraphView(context);
+    }
+
+    @Test
+    public void startsWithDefaultFrameCenter() {
+        assertEquals(0, mRotaryInputGraphView.getFrameCenterPosition(), 0.01);
+    }
+
+    @Test
+    public void addValue_translatesRotaryInputGraphViewWithHighScrollValue() {
+        final float scrollAxisValue = 1000f;
+        final long eventTime = 0;
+
+        mRotaryInputGraphView.addValue(scrollAxisValue, eventTime);
+
+        assertTrue(mRotaryInputGraphView.getFrameCenterPosition() > 0);
+    }
+}
diff --git a/tests/Input/src/com/android/server/input/debug/RotaryInputValueViewTest.java b/tests/Input/src/com/android/server/input/debug/RotaryInputValueViewTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e5e3852dc3183d6b6f44d1998615ae0843ed468c
--- /dev/null
+++ b/tests/Input/src/com/android/server/input/debug/RotaryInputValueViewTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input.debug;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.content.Context;
+import android.view.ViewConfiguration;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+/**
+ * Build/Install/Run:
+ * atest RotaryInputValueViewTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class RotaryInputValueViewTest {
+
+    private final Locale mDefaultLocale = Locale.getDefault();
+
+    private RotaryInputValueView mRotaryInputValueView;
+    private float mScaledVerticalScrollFactor;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        mScaledVerticalScrollFactor =
+                ViewConfiguration.get(context).getScaledVerticalScrollFactor();
+
+        mRotaryInputValueView = new RotaryInputValueView(context);
+    }
+
+    @Test
+    public void startsWithDefaultValue() {
+        assertEquals("+0.0", mRotaryInputValueView.getText().toString());
+    }
+
+    @Test
+    public void updateValue_updatesTextWithScrollValue() {
+        final float scrollAxisValue = 1000f;
+        final String expectedText = String.format(mDefaultLocale, "+%.1f",
+                scrollAxisValue * mScaledVerticalScrollFactor);
+
+        mRotaryInputValueView.updateValue(scrollAxisValue);
+
+        assertEquals(expectedText, mRotaryInputValueView.getText().toString());
+    }
+
+    @Test
+    public void updateActivityStatus_setsAndRemovesColorFilter() {
+        // It should not be active initially.
+        assertNull(mRotaryInputValueView.getBackground().getColorFilter());
+
+        mRotaryInputValueView.updateActivityStatus(true);
+        // It should be active after rotary input.
+        assertNotNull(mRotaryInputValueView.getBackground().getColorFilter());
+
+        mRotaryInputValueView.updateActivityStatus(false);
+        // It should not be active after waiting for mUpdateActivityStatusCallback.
+        assertNull(mRotaryInputValueView.getBackground().getColorFilter());
+    }
+}