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,