diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 73c29d4058cd1..867dafe7f5523 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5452,6 +5452,14 @@ public final class Settings { @Readable public static final String SHOW_TOUCHES = "show_touches"; + /** + * Show key presses and other events dispatched to focused windows on the screen. + * 0 = no + * 1 = yes + * @hide + */ + public static final String SHOW_KEY_PRESSES = "show_key_presses"; + /** * Log raw orientation data from * {@link com.android.server.policy.WindowOrientationListener} for use with the @@ -5842,6 +5850,7 @@ public final class Settings { PRIVATE_SETTINGS.add(NOTIFICATION_LIGHT_PULSE); PRIVATE_SETTINGS.add(POINTER_LOCATION); PRIVATE_SETTINGS.add(SHOW_TOUCHES); + PRIVATE_SETTINGS.add(SHOW_KEY_PRESSES); PRIVATE_SETTINGS.add(WINDOW_ORIENTATION_LISTENER_LOG); PRIVATE_SETTINGS.add(POWER_SOUNDS_ENABLED); PRIVATE_SETTINGS.add(DOCK_SOUNDS_ENABLED); diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 39029896331cf..1af8ca2efe11b 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -4166,6 +4166,40 @@ public final class MotionEvent extends InputEvent implements Parcelable { nativeWriteToParcel(mNativePtr, out); } + /** + * Get the x coordinate of the location where the pointer should be dispatched. + * + * This is required because a mouse event, such as from a touchpad, may contain multiple + * pointers that should all be dispatched to the cursor position. + * @hide + */ + public float getXDispatchLocation(int pointerIndex) { + if (isFromSource(InputDevice.SOURCE_MOUSE)) { + final float xCursorPosition = getXCursorPosition(); + if (xCursorPosition != INVALID_CURSOR_POSITION) { + return xCursorPosition; + } + } + return getX(pointerIndex); + } + + /** + * Get the y coordinate of the location where the pointer should be dispatched. + * + * This is required because a mouse event, such as from a touchpad, may contain multiple + * pointers that should all be dispatched to the cursor position. + * @hide + */ + public float getYDispatchLocation(int pointerIndex) { + if (isFromSource(InputDevice.SOURCE_MOUSE)) { + final float yCursorPosition = getYCursorPosition(); + if (yCursorPosition != INVALID_CURSOR_POSITION) { + return yCursorPosition; + } + } + return getY(pointerIndex); + } + /** * Transfer object for pointer coordinates. * diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f5e4da86bfea8..d4578475e9c3d 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2040,8 +2040,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - final float x = event.getX(pointerIndex); - final float y = event.getY(pointerIndex); + final float x = event.getXDispatchLocation(pointerIndex); + final float y = event.getYDispatchLocation(pointerIndex); if (isOnScrollbarThumb(x, y) || isDraggingScrollBar()) { return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_ARROW); } @@ -2125,8 +2125,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager HoverTarget firstOldHoverTarget = mFirstHoverTarget; mFirstHoverTarget = null; if (!interceptHover && action != MotionEvent.ACTION_HOVER_EXIT) { - final float x = event.getX(); - final float y = event.getY(); + final float x = event.getXDispatchLocation(0); + final float y = event.getYDispatchLocation(0); final int childrenCount = mChildrenCount; if (childrenCount != 0) { final ArrayList preorderedList = buildOrderedChildList(); @@ -2347,8 +2347,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // Check what the child under the pointer says about the tooltip. final int childrenCount = mChildrenCount; if (childrenCount != 0) { - final float x = event.getX(); - final float y = event.getY(); + final float x = event.getXDispatchLocation(0); + final float y = event.getYDispatchLocation(0); final ArrayList preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null @@ -2443,8 +2443,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override protected boolean pointInHoveredChild(MotionEvent event) { if (mFirstHoverTarget != null) { - return isTransformedTouchPointInView(event.getX(), event.getY(), - mFirstHoverTarget.child, null); + return isTransformedTouchPointInView(event.getXDispatchLocation(0), + event.getYDispatchLocation(0), mFirstHoverTarget.child, null); } return false; } @@ -2513,8 +2513,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public boolean onInterceptHoverEvent(MotionEvent event) { if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { final int action = event.getAction(); - final float x = event.getX(); - final float y = event.getY(); + final float x = event.getXDispatchLocation(0); + final float y = event.getYDispatchLocation(0); if ((action == MotionEvent.ACTION_HOVER_MOVE || action == MotionEvent.ACTION_HOVER_ENTER) && isOnScrollbar(x, y)) { return true; @@ -2535,8 +2535,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // Send the event to the child under the pointer. final int childrenCount = mChildrenCount; if (childrenCount != 0) { - final float x = event.getX(); - final float y = event.getY(); + final float x = event.getXDispatchLocation(0); + final float y = event.getXDispatchLocation(0); final ArrayList preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null @@ -2700,10 +2700,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { - final float x = - isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex); - final float y = - isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex); + final float x = ev.getXDispatchLocation(actionIndex); + final float y = ev.getYDispatchLocation(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList preorderedList = buildTouchDispatchChildList(); @@ -2757,8 +2755,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } else { mLastTouchDownIndex = childIndex; } - mLastTouchDownX = ev.getX(); - mLastTouchDownY = ev.getY(); + mLastTouchDownX = x; + mLastTouchDownY = y; newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; @@ -3287,7 +3285,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) - && isOnScrollbarThumb(ev.getX(), ev.getY())) { + && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) { return true; } return false; diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto index 7503dde440e0a..48243f216015d 100644 --- a/core/proto/android/providers/settings/system.proto +++ b/core/proto/android/providers/settings/system.proto @@ -68,6 +68,7 @@ message SystemSettingsProto { // orientationplot.py tool. // 0 = no, 1 = yes optional SettingProto window_orientation_listener_log = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto show_key_presses = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional DevOptions developer_options = 7; diff --git a/core/res/res/drawable/focus_event_pressed_key_background.xml b/core/res/res/drawable/focus_event_pressed_key_background.xml new file mode 100644 index 0000000000000..e069f0bc1300b --- /dev/null +++ b/core/res/res/drawable/focus_event_pressed_key_background.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5dcf284e29fd6..312e2914730a4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5119,4 +5119,6 @@ + + diff --git a/core/tests/coretests/src/android/view/ViewGroupTest.java b/core/tests/coretests/src/android/view/ViewGroupTest.java index 506cc2d3ff971..b37c8fd8c34e4 100644 --- a/core/tests/coretests/src/android/view/ViewGroupTest.java +++ b/core/tests/coretests/src/android/view/ViewGroupTest.java @@ -20,6 +20,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -87,6 +88,9 @@ public class ViewGroupTest { viewGroup.dispatchTouchEvent(event); verify(viewB).dispatchTouchEvent(event); + viewGroup.onResolvePointerIcon(event, 0 /* pointerIndex */); + verify(viewB).onResolvePointerIcon(event, 0); + event = MotionEvent.obtain(0 /* downTime */, 0 /* eventTime */, MotionEvent.ACTION_POINTER_DOWN | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2 /* pointerCount */, properties, coords, 0 /* metaState */, 0 /* buttonState */, @@ -95,7 +99,11 @@ public class ViewGroupTest { viewGroup.dispatchTouchEvent(event); verify(viewB).dispatchTouchEvent(event); + viewGroup.onResolvePointerIcon(event, 1 /* pointerIndex */); + verify(viewB).onResolvePointerIcon(event, 1); + verify(viewA, never()).dispatchTouchEvent(any()); + verify(viewA, never()).onResolvePointerIcon(any(), anyInt()); } /** diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 85623b26c5898..753c860bd5d60 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -183,6 +183,7 @@ public class SystemSettingsValidators { VALIDATORS.put(System.NOTIFICATION_LIGHT_PULSE, BOOLEAN_VALIDATOR); VALIDATORS.put(System.POINTER_LOCATION, BOOLEAN_VALIDATOR); VALIDATORS.put(System.SHOW_TOUCHES, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.SHOW_KEY_PRESSES, BOOLEAN_VALIDATOR); VALIDATORS.put(System.WINDOW_ORIENTATION_LISTENER_LOG, BOOLEAN_VALIDATOR); VALIDATORS.put(System.LOCKSCREEN_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(System.LOCKSCREEN_DISABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index d3a9e91c3da80..1fd84c7191960 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2777,6 +2777,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.System.SHOW_TOUCHES, SystemSettingsProto.DevOptions.SHOW_TOUCHES); + dumpSetting(s, p, + Settings.System.SHOW_KEY_PRESSES, + SystemSettingsProto.DevOptions.SHOW_KEY_PRESSES); dumpSetting(s, p, Settings.System.POINTER_LOCATION, SystemSettingsProto.DevOptions.POINTER_LOCATION); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 2e49dd5eeba9f..73123c20ded2f 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -77,7 +77,8 @@ public class SettingsBackupTest { Settings.System.SCREEN_BRIGHTNESS, // removed in P Settings.System.SETUP_WIZARD_HAS_RUN, // Only used by SuW Settings.System.SHOW_GTALK_SERVICE_STATUS, // candidate for backup? - Settings.System.SHOW_TOUCHES, // bug? + Settings.System.SHOW_TOUCHES, + Settings.System.SHOW_KEY_PRESSES, Settings.System.SIP_ADDRESS_ONLY, // value, not a setting Settings.System.SIP_ALWAYS, // value, not a setting Settings.System.SYSTEM_LOCALES, // bug? diff --git a/services/core/java/com/android/server/input/FocusEventDebugView.java b/services/core/java/com/android/server/input/FocusEventDebugView.java new file mode 100644 index 0000000000000..fba2aa60b9527 --- /dev/null +++ b/services/core/java/com/android/server/input/FocusEventDebugView.java @@ -0,0 +1,343 @@ +/* + * 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 android.util.TypedValue.COMPLEX_UNIT_DIP; +import static android.util.TypedValue.COMPLEX_UNIT_SP; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.animation.LayoutTransition; +import android.annotation.AnyThread; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Typeface; +import android.util.Pair; +import android.util.Slog; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.InputEvent; +import android.view.KeyEvent; +import android.view.RoundedCorner; +import android.view.View; +import android.view.WindowInsets; +import android.view.animation.AccelerateInterpolator; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.R; + +import java.util.HashMap; +import java.util.Map; + +/** + * Displays focus events, such as physical keyboard KeyEvents and non-pointer MotionEvents on + * the screen. + */ +class FocusEventDebugView extends LinearLayout { + + private static final String TAG = FocusEventDebugView.class.getSimpleName(); + + private static final int KEY_FADEOUT_DURATION_MILLIS = 1000; + private static final int KEY_TRANSITION_DURATION_MILLIS = 100; + + private static final int OUTER_PADDING_DP = 16; + private static final int KEY_SEPARATION_MARGIN_DP = 16; + private static final int KEY_VIEW_SIDE_PADDING_DP = 16; + 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 final int mOuterPadding; + + // Tracks all keys that are currently pressed/down. + private final Map, PressedKeyView> + mPressedKeys = new HashMap<>(); + + private final PressedKeyContainer mPressedKeyContainer; + private final PressedKeyContainer mPressedModifierContainer; + + FocusEventDebugView(Context c) { + super(c); + setFocusableInTouchMode(true); + + final var dm = mContext.getResources().getDisplayMetrics(); + mOuterPadding = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, OUTER_PADDING_DP, dm); + + setOrientation(HORIZONTAL); + setLayoutDirection(LAYOUT_DIRECTION_RTL); + setGravity(Gravity.START | Gravity.BOTTOM); + + mPressedKeyContainer = new PressedKeyContainer(mContext); + mPressedKeyContainer.setOrientation(HORIZONTAL); + mPressedKeyContainer.setGravity(Gravity.RIGHT | Gravity.BOTTOM); + mPressedKeyContainer.setLayoutDirection(LAYOUT_DIRECTION_LTR); + final var scroller = new HorizontalScrollView(mContext); + scroller.addView(mPressedKeyContainer); + scroller.setHorizontalScrollBarEnabled(false); + scroller.addOnLayoutChangeListener( + (view, l, t, r, b, ol, ot, or, ob) -> scroller.fullScroll(View.FOCUS_RIGHT)); + scroller.setHorizontalFadingEdgeEnabled(true); + addView(scroller, new LayoutParams(0, WRAP_CONTENT, 1)); + + mPressedModifierContainer = new PressedKeyContainer(mContext); + mPressedModifierContainer.setOrientation(VERTICAL); + mPressedModifierContainer.setGravity(Gravity.LEFT | Gravity.BOTTOM); + addView(mPressedModifierContainer, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + int paddingBottom = 0; + + final RoundedCorner bottomLeft = + insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); + if (bottomLeft != null) { + paddingBottom = bottomLeft.getRadius(); + } + + final RoundedCorner bottomRight = + insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); + if (bottomRight != null) { + paddingBottom = Math.max(paddingBottom, bottomRight.getRadius()); + } + + if (insets.getDisplayCutout() != null) { + paddingBottom = + Math.max(paddingBottom, insets.getDisplayCutout().getSafeInsetBottom()); + } + + setPadding(mOuterPadding, mOuterPadding, mOuterPadding, mOuterPadding + paddingBottom); + setClipToPadding(false); + invalidate(); + return super.onApplyWindowInsets(insets); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + handleKeyEvent(event); + return super.dispatchKeyEvent(event); + } + + /** Report an input event to the debug view. */ + @AnyThread + public void reportEvent(InputEvent event) { + if (!(event instanceof KeyEvent)) { + // TODO: Support non-pointer MotionEvents. + return; + } + post(() -> handleKeyEvent(KeyEvent.obtain((KeyEvent) event))); + } + + private void handleKeyEvent(KeyEvent keyEvent) { + final var identifier = new Pair<>(keyEvent.getDeviceId(), keyEvent.getScanCode()); + final var container = KeyEvent.isModifierKey(keyEvent.getKeyCode()) + ? mPressedModifierContainer + : mPressedKeyContainer; + PressedKeyView pressedKeyView = mPressedKeys.get(identifier); + switch (keyEvent.getAction()) { + case KeyEvent.ACTION_DOWN: { + if (pressedKeyView != null) { + if (keyEvent.getRepeatCount() == 0) { + Slog.w(TAG, "Got key down for " + + KeyEvent.keyCodeToString(keyEvent.getKeyCode()) + + " that was already tracked as being down."); + break; + } + container.handleKeyRepeat(pressedKeyView); + break; + } + + pressedKeyView = new PressedKeyView(mContext, getLabel(keyEvent)); + mPressedKeys.put(identifier, pressedKeyView); + container.handleKeyPressed(pressedKeyView); + break; + } + case KeyEvent.ACTION_UP: { + if (pressedKeyView == null) { + Slog.w(TAG, "Got key up for " + KeyEvent.keyCodeToString(keyEvent.getKeyCode()) + + " that was not tracked as being down."); + break; + } + mPressedKeys.remove(identifier); + container.handleKeyRelease(pressedKeyView); + break; + } + default: + break; + } + keyEvent.recycle(); + } + + private static String getLabel(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_SPACE: + return "\u2423"; + case KeyEvent.KEYCODE_TAB: + return "\u21e5"; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_NUMPAD_ENTER: + return "\u23CE"; + case KeyEvent.KEYCODE_DEL: + return "\u232B"; + case KeyEvent.KEYCODE_FORWARD_DEL: + return "\u2326"; + case KeyEvent.KEYCODE_ESCAPE: + return "ESC"; + case KeyEvent.KEYCODE_DPAD_UP: + return "\u2191"; + case KeyEvent.KEYCODE_DPAD_DOWN: + return "\u2193"; + case KeyEvent.KEYCODE_DPAD_LEFT: + return "\u2190"; + case KeyEvent.KEYCODE_DPAD_RIGHT: + return "\u2192"; + case KeyEvent.KEYCODE_DPAD_UP_RIGHT: + return "\u2197"; + case KeyEvent.KEYCODE_DPAD_UP_LEFT: + return "\u2196"; + case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT: + return "\u2198"; + case KeyEvent.KEYCODE_DPAD_DOWN_LEFT: + return "\u2199"; + default: + break; + } + + final int unicodeChar = event.getUnicodeChar(); + if (unicodeChar != 0) { + return new String(Character.toChars(unicodeChar)); + } + + final var label = KeyEvent.keyCodeToString(event.getKeyCode()); + if (label.startsWith("KEYCODE_")) { + return label.substring(8); + } + return label; + } + + private static class PressedKeyView extends TextView { + + private static final ColorFilter sInvertColors = new ColorMatrixColorFilter(new float[]{ + -1.0f, 0, 0, 0, 255, // red + 0, -1.0f, 0, 0, 255, // green + 0, 0, -1.0f, 0, 255, // blue + 0, 0, 0, 1.0f, 0 // alpha + }); + + PressedKeyView(Context c, String label) { + super(c); + + final var dm = c.getResources().getDisplayMetrics(); + final int keyViewSidePadding = + (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, KEY_VIEW_SIDE_PADDING_DP, dm); + final int keyViewVerticalPadding = + (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, KEY_VIEW_VERTICAL_PADDING_DP, + dm); + final int keyViewMinWidth = + (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, KEY_VIEW_MIN_WIDTH_DP, dm); + final int textSize = + (int) TypedValue.applyDimension(COMPLEX_UNIT_SP, KEY_VIEW_TEXT_SIZE_SP, dm); + + setText(label); + setGravity(Gravity.CENTER); + setMinimumWidth(keyViewMinWidth); + setTextSize(textSize); + setTypeface(Typeface.SANS_SERIF); + setBackgroundResource(R.drawable.focus_event_pressed_key_background); + setPaddingRelative(keyViewSidePadding, keyViewVerticalPadding, keyViewSidePadding, + keyViewVerticalPadding); + + setHighlighted(true); + } + + void setHighlighted(boolean isHighlighted) { + if (isHighlighted) { + setTextColor(Color.BLACK); + getBackground().setColorFilter(sInvertColors); + } else { + setTextColor(Color.WHITE); + getBackground().clearColorFilter(); + } + invalidate(); + } + } + + private static class PressedKeyContainer extends LinearLayout { + + private final MarginLayoutParams mPressedKeyLayoutParams; + + PressedKeyContainer(Context c) { + super(c); + + final var dm = c.getResources().getDisplayMetrics(); + final int keySeparationMargin = + (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, KEY_SEPARATION_MARGIN_DP, dm); + + final var transition = new LayoutTransition(); + transition.disableTransitionType(LayoutTransition.APPEARING); + transition.disableTransitionType(LayoutTransition.DISAPPEARING); + transition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); + transition.setDuration(KEY_TRANSITION_DURATION_MILLIS); + setLayoutTransition(transition); + + mPressedKeyLayoutParams = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); + if (getOrientation() == VERTICAL) { + mPressedKeyLayoutParams.setMargins(0, keySeparationMargin, 0, 0); + } else { + mPressedKeyLayoutParams.setMargins(keySeparationMargin, 0, 0, 0); + } + } + + public void handleKeyPressed(PressedKeyView pressedKeyView) { + addView(pressedKeyView, getChildCount(), mPressedKeyLayoutParams); + invalidate(); + } + + public void handleKeyRepeat(PressedKeyView repeatedKeyView) { + // Do nothing for now. + } + + public void handleKeyRelease(PressedKeyView releasedKeyView) { + releasedKeyView.setHighlighted(false); + releasedKeyView.clearAnimation(); + releasedKeyView.animate() + .alpha(0) + .setDuration(KEY_FADEOUT_DURATION_MILLIS) + .setInterpolator(new AccelerateInterpolator()) + .withEndAction(this::cleanUpPressedKeyViews) + .start(); + } + + private void cleanUpPressedKeyViews() { + int numChildrenToRemove = 0; + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getAlpha() != 0) { + break; + } + child.setVisibility(View.GONE); + child.clearAnimation(); + numChildrenToRemove++; + } + removeViews(0, numChildrenToRemove); + invalidate(); + } + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 5f45f912a87aa..662591e3d2647 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -18,6 +18,7 @@ package com.android.server.input; import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT; import static android.view.KeyEvent.KEYCODE_UNKNOWN; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import android.Manifest; import android.annotation.EnforcePermission; @@ -32,6 +33,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.graphics.PixelFormat; import android.graphics.PointF; import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManager.Sensors; @@ -100,6 +102,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.view.VerifiedInputEvent; import android.view.ViewConfiguration; +import android.view.WindowManager; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -386,6 +389,11 @@ public class InputManagerService extends IInputManager.Stub /** Whether to use the dev/input/event or uevent subsystem for the audio jack. */ final boolean mUseDevInputEventForAudioJack; + private final Object mFocusEventDebugViewLock = new Object(); + @GuardedBy("mFocusEventDebugViewLock") + @Nullable + private FocusEventDebugView mFocusEventDebugView; + /** Point of injection for test dependencies. */ @VisibleForTesting static class Injector { @@ -427,7 +435,7 @@ public class InputManagerService extends IInputManager.Stub mContext = injector.getContext(); mHandler = new InputManagerHandler(injector.getLooper()); mNative = injector.getNativeService(this); - mSettingsObserver = new InputSettingsObserver(mContext, mHandler, mNative); + mSettingsObserver = new InputSettingsObserver(mContext, mHandler, this, mNative); mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore, injector.getLooper()); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); @@ -2460,6 +2468,11 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + synchronized (mFocusEventDebugViewLock) { + if (mFocusEventDebugView != null) { + mFocusEventDebugView.reportEvent(event); + } + } return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags); } @@ -3367,6 +3380,45 @@ public class InputManagerService extends IInputManager.Stub } } + void updateFocusEventDebugViewEnabled(boolean enabled) { + FocusEventDebugView view; + synchronized (mFocusEventDebugViewLock) { + if (enabled == (mFocusEventDebugView != null)) { + return; + } + if (enabled) { + mFocusEventDebugView = new FocusEventDebugView(mContext); + view = mFocusEventDebugView; + } else { + view = mFocusEventDebugView; + mFocusEventDebugView = null; + } + } + Objects.requireNonNull(view); + + // Interact with WM outside the lock, since the lock is part of the input hotpath. + final WindowManager wm = + Objects.requireNonNull(mContext.getSystemService(WindowManager.class)); + if (!enabled) { + wm.removeView(view); + return; + } + + // TODO: Support multi display + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; + lp.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + lp.setFitInsetsTypes(0); + lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + lp.format = PixelFormat.TRANSLUCENT; + lp.setTitle("FocusEventDebugView - display " + mContext.getDisplayId()); + lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; + wm.addView(view, lp); + } + interface KeyboardBacklightControllerInterface { default void incrementKeyboardBacklight(int deviceId) {} default void decrementKeyboardBacklight(int deviceId) {} diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index 153e9c174b859..651063e8841b3 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -43,13 +43,16 @@ class InputSettingsObserver extends ContentObserver { private final Context mContext; private final Handler mHandler; + private final InputManagerService mService; private final NativeInputManagerService mNative; private final Map> mObservers; - InputSettingsObserver(Context context, Handler handler, NativeInputManagerService nativeIms) { + InputSettingsObserver(Context context, Handler handler, InputManagerService service, + NativeInputManagerService nativeIms) { super(handler); mContext = context; mHandler = handler; + mService = service; mNative = nativeIms; mObservers = Map.ofEntries( Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED), @@ -72,7 +75,9 @@ class InputSettingsObserver extends ContentObserver { Map.entry( Settings.Global.getUriFor( Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH), - (reason) -> updateMaximumObscuringOpacityForTouch())); + (reason) -> updateMaximumObscuringOpacityForTouch()), + Map.entry(Settings.System.getUriFor(Settings.System.SHOW_KEY_PRESSES), + (reason) -> updateShowKeyPresses())); } /** @@ -145,6 +150,11 @@ class InputSettingsObserver extends ContentObserver { mNative.setShowTouches(getBoolean(Settings.System.SHOW_TOUCHES, false)); } + private void updateShowKeyPresses() { + mService.updateFocusEventDebugViewEnabled( + getBoolean(Settings.System.SHOW_KEY_PRESSES, false)); + } + private void updateAccessibilityLargePointer() { final int accessibilityConfig = Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,