Merge changes I58a9c06b,I08bedeca into udc-dev
* changes: ViewGroup: Send events to the child at the event's dispatch location Create FocusEventDebugView to show key presses on screen
This commit is contained in:
committed by
Android (Google) Code Review
commit
83f861dc13
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<View> 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<View> 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<View> 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<View> 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
37
core/res/res/drawable/focus_event_pressed_key_background.xml
Normal file
37
core/res/res/drawable/focus_event_pressed_key_background.xml
Normal file
@@ -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>
|
||||
@@ -5119,4 +5119,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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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<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 @@ 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,
|
||||
|
||||
Reference in New Issue
Block a user