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