Loading services/core/java/com/android/server/input/InputManagerService.java +1 −0 Original line number Diff line number Diff line Loading @@ -116,6 +116,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; Loading services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java→services/core/java/com/android/server/input/debug/FocusEventDebugGlobalMonitor.java +2 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading services/core/java/com/android/server/input/FocusEventDebugView.java→services/core/java/com/android/server/input/debug/FocusEventDebugView.java +6 −388 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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(); Loading Loading @@ -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)); } Loading Loading @@ -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)); Loading Loading @@ -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[]{ Loading Loading @@ -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; } } } } services/core/java/com/android/server/input/debug/RotaryInputGraphView.java 0 → 100644 +345 −0 File added.Preview size limit exceeded, changes collapsed. Show changes services/core/java/com/android/server/input/debug/RotaryInputValueView.java 0 → 100644 +102 −0 Original line number Diff line number Diff line /* * 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 com.android.internal.annotations.VisibleForTesting; import java.util.Locale; /** Draws the most recent rotary input value and indicates whether the source is active. */ 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(); 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. */ 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 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); } } Loading
services/core/java/com/android/server/input/InputManagerService.java +1 −0 Original line number Diff line number Diff line Loading @@ -116,6 +116,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; Loading
services/core/java/com/android/server/input/FocusEventDebugGlobalMonitor.java→services/core/java/com/android/server/input/debug/FocusEventDebugGlobalMonitor.java +2 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading
services/core/java/com/android/server/input/FocusEventDebugView.java→services/core/java/com/android/server/input/debug/FocusEventDebugView.java +6 −388 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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(); Loading Loading @@ -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)); } Loading Loading @@ -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)); Loading Loading @@ -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[]{ Loading Loading @@ -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; } } } }
services/core/java/com/android/server/input/debug/RotaryInputGraphView.java 0 → 100644 +345 −0 File added.Preview size limit exceeded, changes collapsed. Show changes
services/core/java/com/android/server/input/debug/RotaryInputValueView.java 0 → 100644 +102 −0 Original line number Diff line number Diff line /* * 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 com.android.internal.annotations.VisibleForTesting; import java.util.Locale; /** Draws the most recent rotary input value and indicates whether the source is active. */ 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(); 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. */ 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 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); } }