diff options
author | 2024-12-13 21:20:59 +0000 | |
---|---|---|
committer | 2025-03-17 21:49:27 +0000 | |
commit | b7e88c955dd08f02f90c8cf98456a411a1bbf9c7 (patch) | |
tree | 65a11afede5515c25d2d9c63d0f79332226bbce8 | |
parent | acbbc9c3ac0d86390c7dea501da3599851338938 (diff) |
Allow search kcm fallbacks
Fixes a bug in where kcm fallbacks with search based modifiers could
not activate since PhoneWindowManager would consume all search events.
This prevented the InputDispatcher from generating the fallback.
This change modifies the behavior so that when a search-based
fallback is detected, the key event will be directly sent to
InputDispatcher to handle the fallback. This will NOT send the
original event to applications.
Bug: 384113980
Test: atest PhoneWindowManagerTest
Test: atest InputTests:InputManagerServiceTests
Flag: com.android.hardware.input.fix_search_modifier_fallbacks
Change-Id: Iacecf945ea6be9841b6009d5f30bcf83f68309ef
6 files changed, 102 insertions, 8 deletions
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 6c2ce3685b30..c41a5ce02e61 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -213,3 +213,12 @@ flag { is_fixed_read_only: true } +flag { + name: "fix_search_modifier_fallbacks" + namespace: "input" + description: "Fixes a bug in which fallbacks from Search based key combinations were not activating." + bug: "384113980" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 29e04e744759..02b399885d81 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -25,6 +25,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.hardware.input.Flags.enableCustomizableInputGestures; +import static com.android.hardware.input.Flags.fixSearchModifierFallbacks; import static com.android.hardware.input.Flags.keyEventActivityDetection; import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; @@ -2653,6 +2654,8 @@ public class InputManagerService extends IInputManager.Stub @SuppressWarnings("unused") @VisibleForTesting long interceptKeyBeforeDispatching(IBinder focus, KeyEvent event, int policyFlags) { + final long keyNotConsumedGoFallback = -2; + final long keyConsumed = -1; final long keyNotConsumed = 0; long value = keyNotConsumed; // TODO(b/358569822) Remove below once we have nicer API for listening to shortcuts @@ -2667,6 +2670,16 @@ public class InputManagerService extends IInputManager.Stub value = mWindowManagerCallbacks.interceptKeyBeforeDispatching(focus, event, policyFlags); } + if (fixSearchModifierFallbacks() && value == keyNotConsumed && event.isMetaPressed()) { + // If the key has not been consumed and includes the meta key, do not send the event + // to the app and attempt to generate a fallback. + final KeyCharacterMap kcm = event.getKeyCharacterMap(); + final KeyCharacterMap.FallbackAction fallbackAction = + kcm.getFallbackAction(event.getKeyCode(), event.getMetaState()); + if (fallbackAction != null) { + return keyNotConsumedGoFallback; + } + } return value; } @@ -3310,9 +3323,10 @@ public class InputManagerService extends IInputManager.Stub * @param token the window token that's about to receive this event * @param event the key event that's being dispatched * @param policyFlags the policy flags - * @return negative value if the key should be skipped (not sent to the app). 0 if the key - * should proceed getting dispatched to the app. positive value to indicate the additional - * time delay, in nanoseconds, to wait before sending this key to the app. + * @return -1 if the key should be skipped (not sent to the app). -2 if the key should not + * be sent to the app, but it should still generate a fallback. + * 0 if the key should proceed getting dispatched to the app. positive value to indicate the + * additional time delay, in nanoseconds, to wait before sending this key to the app. */ long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 2744721c3a46..e9f522d08328 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -86,6 +86,7 @@ import static android.view.contentprotection.flags.Flags.createAccessibilityOver import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; +import static com.android.hardware.input.Flags.fixSearchModifierFallbacks; import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; @@ -4181,6 +4182,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } + if (fixSearchModifierFallbacks()) { + // Pass event as unhandled to give other services, e.g. InputManagerService, the + // opportunity to determine if the event can be modified, e.g. generating a fallback for + // meta/search events. + return false; + } + // Reserve all the META modifier combos for system behavior return (metaState & KeyEvent.META_META_ON) != 0; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index ec8794f8073f..017284cded8e 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -1978,6 +1978,11 @@ NativeInputManager::interceptKeyBeforeDispatching(const sp<IBinder>& token, return inputdispatcher::KeyEntry::InterceptKeyResult::SKIP; } + // -2 : Skip sending even to application and go directly to post processing e.g. fallbacks. + if (delayMillis == -2) { + return inputdispatcher::KeyEntry::InterceptKeyResult::FALLBACK; + } + return milliseconds_to_nanoseconds(delayMillis); } diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index f3d5e39ec127..8d164e1acf74 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -53,14 +53,16 @@ import android.app.AppOpsManager; import android.content.Context; import android.hardware.input.InputManager; import android.os.Bundle; +import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManagerInternal; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; import android.testing.TestableContext; -import android.view.contentprotection.flags.Flags; +import android.view.KeyEvent; import androidx.test.filters.SmallTest; @@ -102,6 +104,8 @@ public class PhoneWindowManagerTests { public final TestableContext mContext = spy( new TestableContext(getInstrumentation().getContext())); + @Mock private IBinder mInputToken; + PhoneWindowManager mPhoneWindowManager; @Mock private ActivityTaskManagerInternal mAtmInternal; @@ -125,6 +129,8 @@ public class PhoneWindowManagerTests { @Mock private LockPatternUtils mLockPatternUtils; + private static final int INTERCEPT_SYSTEM_KEY_NOT_CONSUMED_DELAY = 0; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -216,7 +222,7 @@ public class PhoneWindowManagerTests { @Test public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() { - mSetFlagsRule.enableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); + mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_WALLPAPER, /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); @@ -225,7 +231,7 @@ public class PhoneWindowManagerTests { @Test public void testCheckAddPermission_withAccessibilityOverlay() { - mSetFlagsRule.enableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); + mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY, /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); @@ -234,7 +240,7 @@ public class PhoneWindowManagerTests { @Test public void testCheckAddPermission_withAccessibilityOverlay_flagDisabled() { - mSetFlagsRule.disableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); + mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY, /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); @@ -401,6 +407,35 @@ public class PhoneWindowManagerTests { verify(mDreamManagerInternal).requestDream(); } + @EnableFlags(com.android.hardware.input.Flags.FLAG_FIX_SEARCH_MODIFIER_FALLBACKS) + public void testInterceptKeyBeforeDispatching() { + // Handle sub-tasks of init(). + doNothing().when(mPhoneWindowManager).updateSettings(any()); + doNothing().when(mPhoneWindowManager).initializeHdmiState(); + final DisplayPolicy displayPolicy = mock(DisplayPolicy.class); + mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy; + mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); + final PowerManager pm = mock(PowerManager.class); + doReturn(true).when(pm).isInteractive(); + doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE)); + + mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init( + new PhoneWindowManager.Injector(mContext, + mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0); + + // Case: KeyNotConsumed with meta key. + KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_A, 0, KeyEvent.META_META_ON); + long result = mPhoneWindowManager.interceptKeyBeforeDispatching(mInputToken, keyEvent, 0); + assertEquals(INTERCEPT_SYSTEM_KEY_NOT_CONSUMED_DELAY, result); + + // Case: KeyNotConsumed without meta key. + KeyEvent keyEvent1 = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ESCAPE, 0, 0); + long result1 = mPhoneWindowManager.interceptKeyBeforeDispatching(mInputToken, keyEvent1, 0); + assertEquals(INTERCEPT_SYSTEM_KEY_NOT_CONSUMED_DELAY, result1); + } + private void initPhoneWindowManager() { mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy; mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 7ec8f9ce9864..71c7a6b1119d 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -35,8 +35,8 @@ import android.os.PermissionEnforcer import android.os.SystemClock import android.os.test.FakePermissionEnforcer import android.os.test.TestLooper -import android.platform.test.annotations.Presubmit import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.view.View.OnKeyListener @@ -373,6 +373,29 @@ class InputManagerServiceTests { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_FIX_SEARCH_MODIFIER_FALLBACKS) + fun testInterceptKeyBeforeDispatchingWithFallthroughEvent() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(0) + + // Create a fallback for a key event with a meta modifier. Should result in -2, + // which represents the fallback event, which indicates that original key event will + // be ignored (not sent to app) and instead the fallback will be created and sent to the + // app. + val fallbackAction: KeyCharacterMap.FallbackAction = KeyCharacterMap.FallbackAction.obtain() + fallbackAction.keyCode = KeyEvent.KEYCODE_SEARCH + whenever(kcm.getFallbackAction(anyInt(), anyInt())).thenReturn(fallbackAction) + + val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_META_ON) + assertEquals(-2, service.interceptKeyBeforeDispatching(null, event, 0)) + } + + @Test fun testKeyEventsNotForwardedToFocusedWindow_whenWmConsumes() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( |