Merge changes I58a9c06b,I08bedeca into udc-dev am: 83f861dc13

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/22896325

Change-Id: I01ac720788a30cd4d44ad8397b4d17bcacf7bdad
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 73c29d4..867dafe 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5453,6 +5453,14 @@
         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
          * orientationplot.py tool.
@@ -5842,6 +5850,7 @@
             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 3902989..1af8ca2 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -4167,6 +4167,40 @@
     }
 
     /**
+     * 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.
      *
      * Objects of this type can be used to specify the pointer coordinates when
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index f5e4da8..d457847 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -2040,8 +2040,8 @@
 
     @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 @@
         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<View> preorderedList = buildOrderedChildList();
@@ -2347,8 +2347,8 @@
                 // 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<View> preorderedList = buildOrderedChildList();
                     final boolean customOrder = preorderedList == null
@@ -2443,8 +2443,8 @@
     @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 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 @@
         // 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<View> preorderedList = buildOrderedChildList();
             final boolean customOrder = preorderedList == null
@@ -2700,10 +2700,8 @@
 
                     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<View> preorderedList = buildTouchDispatchChildList();
@@ -2757,8 +2755,8 @@
                                 } else {
                                     mLastTouchDownIndex = childIndex;
                                 }
-                                mLastTouchDownX = ev.getX();
-                                mLastTouchDownY = ev.getY();
+                                mLastTouchDownX = x;
+                                mLastTouchDownY = y;
                                 newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                 alreadyDispatchedToNewTouchTarget = true;
                                 break;
@@ -3287,7 +3285,7 @@
         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 7503dde4..48243f2 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -68,6 +68,7 @@
         // 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 0000000..e069f0b
--- /dev/null
+++ b/core/res/res/drawable/focus_event_pressed_key_background.xml
@@ -0,0 +1,37 @@
+<?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_pressed_key_background"
+       android:shape="rectangle">
+
+  <!-- View background color -->
+  <solid
+      android:color="#AA000000" >
+  </solid>
+
+  <!-- View border color and width -->
+  <stroke
+      android:width="2dp"
+      android:color="@android:color/white">
+  </stroke>
+
+  <!-- The radius makes the corners rounded -->
+  <corners
+      android:radius="8dp">
+  </corners>
+
+</shape>
\ No newline at end of file
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index fa16a8d..43a10e0 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5120,4 +5120,6 @@
   <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Accent" />
   <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Accent.Light" />
   <java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
+
+  <java-symbol type="drawable" name="focus_event_pressed_key_background" />
 </resources>
diff --git a/core/tests/coretests/src/android/view/ViewGroupTest.java b/core/tests/coretests/src/android/view/ViewGroupTest.java
index 506cc2d..b37c8fd 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 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 @@
         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 @@
         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 85623b2..753c860 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 @@
         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 d3a9e91..1fd84c7 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2778,6 +2778,9 @@
                 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);
         dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 2e49dd5..73123c2 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -77,7 +77,8 @@
                     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 0000000..fba2aa6
--- /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<Pair<Integer /*deviceId*/, Integer /*scanCode*/>, 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 5f45f91..662591e 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 @@
 
 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.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.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 @@
     /** 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 @@
         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 @@
     // 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 @@
         }
     }
 
+    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 153e9c1..651063e 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 @@
 
     private final Context mContext;
     private final Handler mHandler;
+    private final InputManagerService mService;
     private final NativeInputManagerService mNative;
     private final Map<Uri, Consumer<String /* reason*/>> 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 @@
                 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 @@
         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,