Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d275e4e9 authored by Iago Mendes's avatar Iago Mendes Committed by Android (Google) Code Review
Browse files

Merge "Create RotaryInputGraphView to display a graph with rotary input values." into main

parents 381c0b8d 101eb73e
Loading
Loading
Loading
Loading
+335 −4
Original line number Diff line number Diff line
@@ -24,9 +24,11 @@ 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;
@@ -50,7 +52,10 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

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;

/**
@@ -70,9 +75,11 @@ class FocusEventDebugView extends RelativeLayout {
    private static final int KEY_VIEW_VERTICAL_PADDING_DP = 8;
    private static final int KEY_VIEW_MIN_WIDTH_DP = 32;
    private static final int KEY_VIEW_TEXT_SIZE_SP = 12;
    private static final double ROTATY_GRAPH_HEIGHT_FRACTION = 0.5;

    private final InputManagerService mService;
    private final int mOuterPadding;
    private final DisplayMetrics mDm;

    // Tracks all keys that are currently pressed/down.
    private final Map<Pair<Integer /*deviceId*/, Integer /*scanCode*/>, PressedKeyView>
@@ -87,21 +94,26 @@ class FocusEventDebugView extends RelativeLayout {
    private final Supplier<RotaryInputValueView> mRotaryInputValueViewFactory;
    @Nullable
    private RotaryInputValueView mRotaryInputValueView;
    private final Supplier<RotaryInputGraphView> mRotaryInputGraphViewFactory;
    @Nullable
    private RotaryInputGraphView mRotaryInputGraphView;

    @VisibleForTesting
    FocusEventDebugView(Context c, InputManagerService service,
            Supplier<RotaryInputValueView> rotaryInputValueViewFactory) {
            Supplier<RotaryInputValueView> rotaryInputValueViewFactory,
            Supplier<RotaryInputGraphView> rotaryInputGraphViewFactory) {
        super(c);
        setFocusableInTouchMode(true);

        mService = service;
        mRotaryInputValueViewFactory = rotaryInputValueViewFactory;
        final var dm = mContext.getResources().getDisplayMetrics();
        mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm);
        mRotaryInputGraphViewFactory = rotaryInputGraphViewFactory;
        mDm = mContext.getResources().getDisplayMetrics();
        mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, mDm);
    }

    FocusEventDebugView(Context c, InputManagerService service) {
        this(c, service, () -> new RotaryInputValueView(c));
        this(c, service, () -> new RotaryInputValueView(c), () -> new RotaryInputGraphView(c));
    }

    @Override
@@ -196,6 +208,8 @@ class FocusEventDebugView extends RelativeLayout {
            mFocusEventDebugGlobalMonitor = null;
            removeView(mRotaryInputValueView);
            mRotaryInputValueView = null;
            removeView(mRotaryInputGraphView);
            mRotaryInputGraphView = null;
            return;
        }

@@ -206,6 +220,12 @@ class FocusEventDebugView extends RelativeLayout {
        valueLayoutParams.addRule(CENTER_HORIZONTAL);
        valueLayoutParams.addRule(ALIGN_PARENT_BOTTOM);
        addView(mRotaryInputValueView, valueLayoutParams);

        mRotaryInputGraphView = mRotaryInputGraphViewFactory.get();
        LayoutParams graphLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
                (int) (ROTATY_GRAPH_HEIGHT_FRACTION * mDm.heightPixels));
        graphLayoutParams.addRule(CENTER_IN_PARENT);
        addView(mRotaryInputGraphView, graphLayoutParams);
    }

    /** Report a key event to the debug view. */
@@ -276,6 +296,7 @@ class FocusEventDebugView extends RelativeLayout {

        float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
        mRotaryInputValueView.updateValue(scrollAxisValue);
        mRotaryInputGraphView.addValue(scrollAxisValue, motionEvent.getEventTime());

        motionEvent.recycle();
    }
@@ -453,6 +474,8 @@ class FocusEventDebugView extends RelativeLayout {
        }
    }

    // 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 {
@@ -514,4 +537,312 @@ class FocusEventDebugView extends RelativeLayout {
            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;
            }
        }
    }
}
+18 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.server.input;
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.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.mock;
@@ -48,6 +49,7 @@ public class FocusEventDebugViewTest {

    private FocusEventDebugView mFocusEventDebugView;
    private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView;
    private FocusEventDebugView.RotaryInputGraphView mRotaryInputGraphView;
    private float mScaledVerticalScrollFactor;

    @Before
@@ -60,8 +62,9 @@ public class FocusEventDebugViewTest {
                .thenReturn(InputChannel.openInputChannelPair("FocusEventDebugViewTest")[1]);

        mRotaryInputValueView = new FocusEventDebugView.RotaryInputValueView(context);
        mRotaryInputGraphView = new FocusEventDebugView.RotaryInputGraphView(context);
        mFocusEventDebugView = new FocusEventDebugView(context, mockService,
                () -> mRotaryInputValueView);
                () -> mRotaryInputValueView, () -> mRotaryInputGraphView);
    }

    @Test
@@ -69,6 +72,11 @@ public class FocusEventDebugViewTest {
        assertEquals("+0.0", mRotaryInputValueView.getText());
    }

    @Test
    public void startsRotaryInputGraphViewWithDefaultFrameCenter() {
        assertEquals(0, mRotaryInputGraphView.getFrameCenterPosition(), 0.01);
    }

    @Test
    public void handleRotaryInput_updatesRotaryInputValueViewWithScrollValue() {
        mFocusEventDebugView.handleUpdateShowRotaryInput(true);
@@ -79,6 +87,15 @@ public class FocusEventDebugViewTest {
                mRotaryInputValueView.getText());
    }

    @Test
    public void handleRotaryInput_translatesRotaryInputGraphViewWithHighScrollValue() {
        mFocusEventDebugView.handleUpdateShowRotaryInput(true);

        mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(1000f));

        assertTrue(mRotaryInputGraphView.getFrameCenterPosition() > 0);
    }

    @Test
    public void updateActivityStatus_setsAndRemovesColorFilter() {
        // It should not be active initially.