Loading core/res/res/drawable/focus_event_rotary_input_background.xml 0 → 100644 +30 −0 Original line number Original line Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:name="focus_event_rotary_input_background" android:shape="rectangle"> <!-- View background color --> <solid android:color="#80741b47" /> <!-- View border color and width --> <stroke android:width="1dp" android:color="#ffff00ff" /> <!-- The radius makes the corners rounded --> <corners android:radius="4dp" /> </shape> No newline at end of file core/res/res/values/symbols.xml +1 −0 Original line number Original line Diff line number Diff line Loading @@ -5197,6 +5197,7 @@ <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" /> <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" /> <java-symbol type="drawable" name="focus_event_pressed_key_background" /> <java-symbol type="drawable" name="focus_event_pressed_key_background" /> <java-symbol type="drawable" name="focus_event_rotary_input_background" /> <java-symbol type="string" name="config_defaultShutdownVibrationFile" /> <java-symbol type="string" name="config_defaultShutdownVibrationFile" /> <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" /> <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" /> Loading services/core/java/com/android/server/input/FocusEventDebugView.java +112 −17 Original line number Original line Diff line number Diff line Loading @@ -28,7 +28,7 @@ import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.ColorFilter; import android.graphics.ColorMatrixColorFilter; import android.graphics.ColorMatrixColorFilter; import android.graphics.Typeface; import android.graphics.Typeface; import android.util.Log; import android.util.DisplayMetrics; import android.util.Pair; import android.util.Pair; import android.util.Slog; import android.util.Slog; import android.util.TypedValue; import android.util.TypedValue; Loading @@ -38,22 +38,26 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.RoundedCorner; import android.view.RoundedCorner; import android.view.View; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowInsets; import android.view.animation.AccelerateInterpolator; import android.view.animation.AccelerateInterpolator; import android.widget.HorizontalScrollView; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.TextView; import com.android.internal.R; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.HashMap; import java.util.Map; import java.util.Map; import java.util.function.Supplier; /** /** * Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on * Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on * the screen. * the screen. */ */ class FocusEventDebugView extends LinearLayout { class FocusEventDebugView extends RelativeLayout { private static final String TAG = FocusEventDebugView.class.getSimpleName(); private static final String TAG = FocusEventDebugView.class.getSimpleName(); Loading @@ -80,18 +84,24 @@ class FocusEventDebugView extends LinearLayout { private PressedKeyContainer mPressedKeyContainer; private PressedKeyContainer mPressedKeyContainer; @Nullable @Nullable private PressedKeyContainer mPressedModifierContainer; private PressedKeyContainer mPressedModifierContainer; private final Supplier<RotaryInputValueView> mRotaryInputValueViewFactory; @Nullable private RotaryInputValueView mRotaryInputValueView; FocusEventDebugView(Context c, InputManagerService service) { @VisibleForTesting FocusEventDebugView(Context c, InputManagerService service, Supplier<RotaryInputValueView> rotaryInputValueViewFactory) { super(c); super(c); setFocusableInTouchMode(true); setFocusableInTouchMode(true); mService = service; mService = service; mRotaryInputValueViewFactory = rotaryInputValueViewFactory; final var dm = mContext.getResources().getDisplayMetrics(); final var dm = mContext.getResources().getDisplayMetrics(); mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); } setOrientation(HORIZONTAL); FocusEventDebugView(Context c, InputManagerService service) { setLayoutDirection(LAYOUT_DIRECTION_RTL); this(c, service, () -> new RotaryInputValueView(c)); setGravity(Gravity.START | Gravity.BOTTOM); } } @Override @Override Loading @@ -100,13 +110,13 @@ class FocusEventDebugView extends LinearLayout { final RoundedCorner bottomLeft = final RoundedCorner bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); if (bottomLeft != null) { if (bottomLeft != null && !insets.isRound()) { paddingBottom = bottomLeft.getRadius(); paddingBottom = bottomLeft.getRadius(); } } final RoundedCorner bottomRight = final RoundedCorner bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); if (bottomRight != null) { if (bottomRight != null && !insets.isRound()) { paddingBottom = Math.max(paddingBottom, bottomRight.getRadius()); paddingBottom = Math.max(paddingBottom, bottomRight.getRadius()); } } Loading Loading @@ -151,7 +161,7 @@ class FocusEventDebugView extends LinearLayout { } } mPressedKeyContainer = new PressedKeyContainer(mContext); mPressedKeyContainer = new PressedKeyContainer(mContext); mPressedKeyContainer.setOrientation(HORIZONTAL); mPressedKeyContainer.setOrientation(LinearLayout.HORIZONTAL); mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM); mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM); mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR); mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR); final var scroller = new HorizontalScrollView(mContext); final var scroller = new HorizontalScrollView(mContext); Loading @@ -160,15 +170,23 @@ class FocusEventDebugView extends LinearLayout { scroller.addOnLayoutChangeListener( scroller.addOnLayoutChangeListener( (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT)); (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT)); scroller.setHorizontalFadingEdgeEnabled(true); scroller.setHorizontalFadingEdgeEnabled(true); addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1)); LayoutParams scrollerLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); scrollerLayoutParams.addRule(ALIGN_PARENT_BOTTOM); scrollerLayoutParams.addRule(ALIGN_PARENT_RIGHT); addView(scroller, scrollerLayoutParams); mPressedModifierContainer = new PressedKeyContainer(mContext); mPressedModifierContainer = new PressedKeyContainer(mContext); mPressedModifierContainer.setOrientation(VERTICAL); mPressedModifierContainer.setOrientation(LinearLayout.VERTICAL); mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM); mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM); addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); LayoutParams modifierLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); modifierLayoutParams.addRule(ALIGN_PARENT_BOTTOM); modifierLayoutParams.addRule(ALIGN_PARENT_LEFT); modifierLayoutParams.addRule(LEFT_OF, scroller.getId()); addView(mPressedModifierContainer, modifierLayoutParams); } } private void handleUpdateShowRotaryInput(boolean enabled) { @VisibleForTesting void handleUpdateShowRotaryInput(boolean enabled) { if (enabled == showRotaryInput()) { if (enabled == showRotaryInput()) { return; return; } } Loading @@ -176,10 +194,18 @@ class FocusEventDebugView extends LinearLayout { if (!enabled) { if (!enabled) { mFocusEventDebugGlobalMonitor.dispose(); mFocusEventDebugGlobalMonitor.dispose(); mFocusEventDebugGlobalMonitor = null; mFocusEventDebugGlobalMonitor = null; removeView(mRotaryInputValueView); mRotaryInputValueView = null; return; return; } } mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService); mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService); mRotaryInputValueView = mRotaryInputValueViewFactory.get(); LayoutParams valueLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); valueLayoutParams.addRule(CENTER_HORIZONTAL); valueLayoutParams.addRule(ALIGN_PARENT_BOTTOM); addView(mRotaryInputValueView, valueLayoutParams); } } /** Report a key event to the debug view. */ /** Report a key event to the debug view. */ Loading Loading @@ -242,14 +268,14 @@ class FocusEventDebugView extends LinearLayout { keyEvent.recycle(); keyEvent.recycle(); } } private void handleRotaryInput(MotionEvent motionEvent) { @VisibleForTesting void handleRotaryInput(MotionEvent motionEvent) { if (!showRotaryInput()) { if (!showRotaryInput()) { return; return; } } float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); // TODO(b/286086154): replace log with visualization. mRotaryInputValueView.updateValue(scrollAxisValue); Log.d(TAG, "ROTARY INPUT: " + String.valueOf(scrollAxisValue)); motionEvent.recycle(); motionEvent.recycle(); } } Loading Loading @@ -308,7 +334,14 @@ class FocusEventDebugView extends LinearLayout { /** Determine whether to show rotary input by checking one of the rotary-related objects. */ /** Determine whether to show rotary input by checking one of the rotary-related objects. */ private boolean showRotaryInput() { private boolean showRotaryInput() { return mFocusEventDebugGlobalMonitor != null; 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 class PressedKeyView extends TextView { Loading Loading @@ -419,4 +452,66 @@ class FocusEventDebugView extends LinearLayout { invalidate(); invalidate(); } } } } /** 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)); } } } } tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java 0 → 100644 +118 −0 Original line number Original line 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; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.view.InputChannel; 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 org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Build/Install/Run: * atest FocusEventDebugViewTest */ @RunWith(AndroidJUnit4.class) public class FocusEventDebugViewTest { private FocusEventDebugView mFocusEventDebugView; private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView; private float mScaledVerticalScrollFactor; @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); mFocusEventDebugView = new FocusEventDebugView(context, mockService, () -> mRotaryInputValueView); } @Test public void startsRotaryInputValueViewWithDefaultValue() { assertEquals("+0.0", mRotaryInputValueView.getText()); } @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 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()); } private MotionEvent createRotaryMotionEvent(float scrollAxisValue) { PointerCoords pointerCoords = new PointerCoords(); pointerCoords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAxisValue); PointerProperties pointerProperties = new PointerProperties(); return MotionEvent.obtain( /* downTime */ 0, /* eventTime */ 0, /* action */ MotionEvent.ACTION_SCROLL, /* pointerCount */ 1, /* pointerProperties */ new PointerProperties[] {pointerProperties}, /* pointerCoords */ new PointerCoords[] {pointerCoords}, /* metaState */ 0, /* buttonState */ 0, /* xPrecision */ 0, /* yPrecision */ 0, /* deviceId */ 0, /* edgeFlags */ 0, /* source */ InputDevice.SOURCE_ROTARY_ENCODER, /* flags */ 0 ); } } Loading
core/res/res/drawable/focus_event_rotary_input_background.xml 0 → 100644 +30 −0 Original line number Original line Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- 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. --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:name="focus_event_rotary_input_background" android:shape="rectangle"> <!-- View background color --> <solid android:color="#80741b47" /> <!-- View border color and width --> <stroke android:width="1dp" android:color="#ffff00ff" /> <!-- The radius makes the corners rounded --> <corners android:radius="4dp" /> </shape> No newline at end of file
core/res/res/values/symbols.xml +1 −0 Original line number Original line Diff line number Diff line Loading @@ -5197,6 +5197,7 @@ <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" /> <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" /> <java-symbol type="drawable" name="focus_event_pressed_key_background" /> <java-symbol type="drawable" name="focus_event_pressed_key_background" /> <java-symbol type="drawable" name="focus_event_rotary_input_background" /> <java-symbol type="string" name="config_defaultShutdownVibrationFile" /> <java-symbol type="string" name="config_defaultShutdownVibrationFile" /> <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" /> <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" /> Loading
services/core/java/com/android/server/input/FocusEventDebugView.java +112 −17 Original line number Original line Diff line number Diff line Loading @@ -28,7 +28,7 @@ import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.ColorFilter; import android.graphics.ColorMatrixColorFilter; import android.graphics.ColorMatrixColorFilter; import android.graphics.Typeface; import android.graphics.Typeface; import android.util.Log; import android.util.DisplayMetrics; import android.util.Pair; import android.util.Pair; import android.util.Slog; import android.util.Slog; import android.util.TypedValue; import android.util.TypedValue; Loading @@ -38,22 +38,26 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.RoundedCorner; import android.view.RoundedCorner; import android.view.View; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowInsets; import android.view.animation.AccelerateInterpolator; import android.view.animation.AccelerateInterpolator; import android.widget.HorizontalScrollView; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.TextView; import com.android.internal.R; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.HashMap; import java.util.HashMap; import java.util.Map; import java.util.Map; import java.util.function.Supplier; /** /** * Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on * Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on * the screen. * the screen. */ */ class FocusEventDebugView extends LinearLayout { class FocusEventDebugView extends RelativeLayout { private static final String TAG = FocusEventDebugView.class.getSimpleName(); private static final String TAG = FocusEventDebugView.class.getSimpleName(); Loading @@ -80,18 +84,24 @@ class FocusEventDebugView extends LinearLayout { private PressedKeyContainer mPressedKeyContainer; private PressedKeyContainer mPressedKeyContainer; @Nullable @Nullable private PressedKeyContainer mPressedModifierContainer; private PressedKeyContainer mPressedModifierContainer; private final Supplier<RotaryInputValueView> mRotaryInputValueViewFactory; @Nullable private RotaryInputValueView mRotaryInputValueView; FocusEventDebugView(Context c, InputManagerService service) { @VisibleForTesting FocusEventDebugView(Context c, InputManagerService service, Supplier<RotaryInputValueView> rotaryInputValueViewFactory) { super(c); super(c); setFocusableInTouchMode(true); setFocusableInTouchMode(true); mService = service; mService = service; mRotaryInputValueViewFactory = rotaryInputValueViewFactory; final var dm = mContext.getResources().getDisplayMetrics(); final var dm = mContext.getResources().getDisplayMetrics(); mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); } setOrientation(HORIZONTAL); FocusEventDebugView(Context c, InputManagerService service) { setLayoutDirection(LAYOUT_DIRECTION_RTL); this(c, service, () -> new RotaryInputValueView(c)); setGravity(Gravity.START | Gravity.BOTTOM); } } @Override @Override Loading @@ -100,13 +110,13 @@ class FocusEventDebugView extends LinearLayout { final RoundedCorner bottomLeft = final RoundedCorner bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); if (bottomLeft != null) { if (bottomLeft != null && !insets.isRound()) { paddingBottom = bottomLeft.getRadius(); paddingBottom = bottomLeft.getRadius(); } } final RoundedCorner bottomRight = final RoundedCorner bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); if (bottomRight != null) { if (bottomRight != null && !insets.isRound()) { paddingBottom = Math.max(paddingBottom, bottomRight.getRadius()); paddingBottom = Math.max(paddingBottom, bottomRight.getRadius()); } } Loading Loading @@ -151,7 +161,7 @@ class FocusEventDebugView extends LinearLayout { } } mPressedKeyContainer = new PressedKeyContainer(mContext); mPressedKeyContainer = new PressedKeyContainer(mContext); mPressedKeyContainer.setOrientation(HORIZONTAL); mPressedKeyContainer.setOrientation(LinearLayout.HORIZONTAL); mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM); mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM); mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR); mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR); final var scroller = new HorizontalScrollView(mContext); final var scroller = new HorizontalScrollView(mContext); Loading @@ -160,15 +170,23 @@ class FocusEventDebugView extends LinearLayout { scroller.addOnLayoutChangeListener( scroller.addOnLayoutChangeListener( (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT)); (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT)); scroller.setHorizontalFadingEdgeEnabled(true); scroller.setHorizontalFadingEdgeEnabled(true); addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1)); LayoutParams scrollerLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); scrollerLayoutParams.addRule(ALIGN_PARENT_BOTTOM); scrollerLayoutParams.addRule(ALIGN_PARENT_RIGHT); addView(scroller, scrollerLayoutParams); mPressedModifierContainer = new PressedKeyContainer(mContext); mPressedModifierContainer = new PressedKeyContainer(mContext); mPressedModifierContainer.setOrientation(VERTICAL); mPressedModifierContainer.setOrientation(LinearLayout.VERTICAL); mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM); mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM); addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); LayoutParams modifierLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); modifierLayoutParams.addRule(ALIGN_PARENT_BOTTOM); modifierLayoutParams.addRule(ALIGN_PARENT_LEFT); modifierLayoutParams.addRule(LEFT_OF, scroller.getId()); addView(mPressedModifierContainer, modifierLayoutParams); } } private void handleUpdateShowRotaryInput(boolean enabled) { @VisibleForTesting void handleUpdateShowRotaryInput(boolean enabled) { if (enabled == showRotaryInput()) { if (enabled == showRotaryInput()) { return; return; } } Loading @@ -176,10 +194,18 @@ class FocusEventDebugView extends LinearLayout { if (!enabled) { if (!enabled) { mFocusEventDebugGlobalMonitor.dispose(); mFocusEventDebugGlobalMonitor.dispose(); mFocusEventDebugGlobalMonitor = null; mFocusEventDebugGlobalMonitor = null; removeView(mRotaryInputValueView); mRotaryInputValueView = null; return; return; } } mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService); mFocusEventDebugGlobalMonitor = new FocusEventDebugGlobalMonitor(this, mService); mRotaryInputValueView = mRotaryInputValueViewFactory.get(); LayoutParams valueLayoutParams = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); valueLayoutParams.addRule(CENTER_HORIZONTAL); valueLayoutParams.addRule(ALIGN_PARENT_BOTTOM); addView(mRotaryInputValueView, valueLayoutParams); } } /** Report a key event to the debug view. */ /** Report a key event to the debug view. */ Loading Loading @@ -242,14 +268,14 @@ class FocusEventDebugView extends LinearLayout { keyEvent.recycle(); keyEvent.recycle(); } } private void handleRotaryInput(MotionEvent motionEvent) { @VisibleForTesting void handleRotaryInput(MotionEvent motionEvent) { if (!showRotaryInput()) { if (!showRotaryInput()) { return; return; } } float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); float scrollAxisValue = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); // TODO(b/286086154): replace log with visualization. mRotaryInputValueView.updateValue(scrollAxisValue); Log.d(TAG, "ROTARY INPUT: " + String.valueOf(scrollAxisValue)); motionEvent.recycle(); motionEvent.recycle(); } } Loading Loading @@ -308,7 +334,14 @@ class FocusEventDebugView extends LinearLayout { /** Determine whether to show rotary input by checking one of the rotary-related objects. */ /** Determine whether to show rotary input by checking one of the rotary-related objects. */ private boolean showRotaryInput() { private boolean showRotaryInput() { return mFocusEventDebugGlobalMonitor != null; 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 class PressedKeyView extends TextView { Loading Loading @@ -419,4 +452,66 @@ class FocusEventDebugView extends LinearLayout { invalidate(); invalidate(); } } } } /** 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)); } } } }
tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java 0 → 100644 +118 −0 Original line number Original line 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; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.view.InputChannel; 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 org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Build/Install/Run: * atest FocusEventDebugViewTest */ @RunWith(AndroidJUnit4.class) public class FocusEventDebugViewTest { private FocusEventDebugView mFocusEventDebugView; private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView; private float mScaledVerticalScrollFactor; @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); mFocusEventDebugView = new FocusEventDebugView(context, mockService, () -> mRotaryInputValueView); } @Test public void startsRotaryInputValueViewWithDefaultValue() { assertEquals("+0.0", mRotaryInputValueView.getText()); } @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 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()); } private MotionEvent createRotaryMotionEvent(float scrollAxisValue) { PointerCoords pointerCoords = new PointerCoords(); pointerCoords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAxisValue); PointerProperties pointerProperties = new PointerProperties(); return MotionEvent.obtain( /* downTime */ 0, /* eventTime */ 0, /* action */ MotionEvent.ACTION_SCROLL, /* pointerCount */ 1, /* pointerProperties */ new PointerProperties[] {pointerProperties}, /* pointerCoords */ new PointerCoords[] {pointerCoords}, /* metaState */ 0, /* buttonState */ 0, /* xPrecision */ 0, /* yPrecision */ 0, /* deviceId */ 0, /* edgeFlags */ 0, /* source */ InputDevice.SOURCE_ROTARY_ENCODER, /* flags */ 0 ); } }