diff options
6 files changed, 265 insertions, 4 deletions
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 7fc7e4d81afa..1c2150f3c09f 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -34,6 +34,7 @@ import android.hardware.input.KeyboardLayoutSelectionResult; import android.hardware.input.TouchCalibration; import android.os.CombinedVibration; import android.hardware.input.IInputSensorEventListener; +import android.hardware.input.IKeyEventActivityListener; import android.hardware.input.InputSensorInfo; import android.hardware.input.KeyGlyphMap; import android.hardware.lights.Light; @@ -213,6 +214,16 @@ interface IInputManager { void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); + @EnforcePermission("LISTEN_FOR_KEY_ACTIVITY") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)") + boolean registerKeyEventActivityListener(IKeyEventActivityListener listener); + + @EnforcePermission("LISTEN_FOR_KEY_ACTIVITY") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)") + boolean unregisterKeyEventActivityListener(IKeyEventActivityListener listener); + // Get the bluetooth address of an input device if known, returning null if it either is not // connected via bluetooth or if the address cannot be determined. @EnforcePermission("BLUETOOTH") diff --git a/core/java/android/hardware/input/IKeyEventActivityListener.aidl b/core/java/android/hardware/input/IKeyEventActivityListener.aidl new file mode 100644 index 000000000000..b3097d409a2d --- /dev/null +++ b/core/java/android/hardware/input/IKeyEventActivityListener.aidl @@ -0,0 +1,23 @@ +/* + * Copyright 2025 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 android.hardware.input; + +/** @hide */ +oneway interface IKeyEventActivityListener +{ + void onKeyEventActivity(); +} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index cf41e138047a..0ead8232c5b1 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1776,4 +1776,41 @@ public final class InputManager { */ boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType); } + + /** @hide */ + public interface KeyEventActivityListener { + /** + * Reports a change for user activeness. + * + * This listener will be triggered any time a user presses a key. + */ + void onKeyEventActivity(); + } + + + /** + * Registers a listener for updates to key event activeness + * + * @param listener to be registered + * @return true if listener registered successfully + * @hide + */ + @RequiresPermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) + public boolean registerKeyEventActivityListener(@NonNull KeyEventActivityListener listener) { + return mGlobal.registerKeyEventActivityListener(listener); + } + + /** + * Unregisters a listener for updates to key event activeness + * + * @param listener to be unregistered + * @return true if listener unregistered successfully, also returns true if + * invoked but listener was not present + * @hide + */ + @RequiresPermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) + public boolean unregisterKeyEventActivityListener(@NonNull KeyEventActivityListener listener) { + return mGlobal.unregisterKeyEventActivityListener(listener); + } + } diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index e79416162fc2..a9a45ae45ec3 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -26,6 +26,7 @@ import android.hardware.SensorManager; import android.hardware.input.InputManager.InputDeviceBatteryListener; import android.hardware.input.InputManager.InputDeviceListener; import android.hardware.input.InputManager.KeyGestureEventHandler; +import android.hardware.input.InputManager.KeyEventActivityListener; import android.hardware.input.InputManager.KeyGestureEventListener; import android.hardware.input.InputManager.KeyboardBacklightListener; import android.hardware.input.InputManager.OnTabletModeChangedListener; @@ -124,6 +125,13 @@ public final class InputManagerGlobal { @Nullable private IKeyGestureEventListener mKeyGestureEventListener; + private final Object mKeyEventActivityLock = new Object(); + @GuardedBy("mKeyEventActivityLock") + private ArrayList<KeyEventActivityListener> mKeyEventActivityListeners; + @GuardedBy("mKeyEventActivityLock") + @Nullable + private IKeyEventActivityListener mKeyEventActivityListener; + private final Object mKeyGestureEventHandlerLock = new Object(); @GuardedBy("mKeyGestureEventHandlerLock") @Nullable @@ -1257,6 +1265,63 @@ public final class InputManagerGlobal { } } + private class LocalKeyEventActivityListener extends IKeyEventActivityListener.Stub { + @Override + public void onKeyEventActivity() { + synchronized (mKeyEventActivityLock) { + final int numListeners = mKeyEventActivityListeners.size(); + for (int i = 0; i < numListeners; i++) { + KeyEventActivityListener listener = mKeyEventActivityListeners.get(i); + listener.onKeyEventActivity(); + } + } + } + } + + boolean registerKeyEventActivityListener(@NonNull KeyEventActivityListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + boolean success = false; + synchronized (mKeyEventActivityLock) { + if (mKeyEventActivityListener == null) { + mKeyEventActivityListeners = new ArrayList<>(); + mKeyEventActivityListener = new LocalKeyEventActivityListener(); + + try { + success = mIm.registerKeyEventActivityListener(mKeyEventActivityListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + if (mKeyEventActivityListeners.contains(listener)) { + throw new IllegalArgumentException("Listener has already been registered!"); + } + mKeyEventActivityListeners.add(listener); + return success; + } + } + + boolean unregisterKeyEventActivityListener(@NonNull KeyEventActivityListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + boolean success = true; + synchronized (mKeyEventActivityLock) { + if (mKeyEventActivityListeners == null) { + return success; + } + mKeyEventActivityListeners.remove(listener); + if (mKeyEventActivityListeners.isEmpty()) { + try { + success = mIm.unregisterKeyEventActivityListener(mKeyEventActivityListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mKeyEventActivityListeners = null; + mKeyEventActivityListener = null; + } + } + return success; + } + /** * Sets the keyboard layout override for the specified input device. This will set the * keyboard layout as the default for the input device irrespective of the underlying IME diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index af021e5f515b..2ad5a1538da9 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.touchpadVisualizer; +import static com.android.hardware.input.Flags.keyEventActivityDetection; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER; @@ -61,6 +62,7 @@ import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; +import android.hardware.input.IKeyEventActivityListener; import android.hardware.input.IKeyGestureEventListener; import android.hardware.input.IKeyGestureHandler; import android.hardware.input.IKeyboardBacklightListener; @@ -89,11 +91,13 @@ import android.os.InputEventInjectionResult; import android.os.InputEventInjectionSync; import android.os.Looper; import android.os.Message; +import android.os.PermissionEnforcer; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; +import android.os.SystemClock; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.vibrator.StepSegment; @@ -280,6 +284,16 @@ public class InputManagerService extends IInputManager.Stub @GuardedBy("mAssociationsLock") private final Map<String, Integer> mRuntimeAssociations = new ArrayMap<>(); + final Object mKeyEventActivityLock = new Object(); + @GuardedBy("mKeyEventActivityLock") + private List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify = + new ArrayList<>(); + + // Rate limit for key event activity detection. Prevent the listener from being notified + // too frequently. + private static final long KEY_EVENT_ACTIVITY_RATE_LIMIT_INTERVAL_MS = 1000; + private long mLastKeyEventActivityTimeMs = 0; + // The associations of input devices to displays by port. Maps from {InputDevice#mName} (String) // to {DisplayInfo#uniqueId} (String) so that events from the Input Device go to a // specific display. @@ -484,13 +498,16 @@ public class InputManagerService extends IInputManager.Stub } public InputManagerService(Context context) { - this(new Injector(context, DisplayThread.get().getLooper(), new UEventManager() {})); + this(new Injector(context, DisplayThread.get().getLooper(), new UEventManager() {}), + context.getSystemService(PermissionEnforcer.class)); } @VisibleForTesting - InputManagerService(Injector injector) { + InputManagerService(Injector injector, PermissionEnforcer permissionEnforcer) { // The static association map is accessed by both java and native code, so it must be // initialized before initializing the native service. + super(permissionEnforcer); + mStaticAssociations = loadStaticInputPortAssociations(); mContext = injector.getContext(); @@ -2509,9 +2526,73 @@ public class InputManagerService extends IInputManager.Stub return true; } + @EnforcePermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) + @Override // Binder Call + public boolean registerKeyEventActivityListener(@NonNull IKeyEventActivityListener listener) { + super.registerKeyEventActivityListener_enforcePermission(); + Objects.requireNonNull(listener, "listener must not be null"); + return InputManagerService.this.registerKeyEventActivityListenerInternal(listener); + } + + @EnforcePermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) + @Override // Binder Call + public boolean unregisterKeyEventActivityListener(@NonNull IKeyEventActivityListener listener) { + super.unregisterKeyEventActivityListener_enforcePermission(); + Objects.requireNonNull(listener, "listener must not be null"); + return InputManagerService.this.unregisterKeyEventActivityListenerInternal(listener); + } + + /** + * Registers a listener for updates to key event activeness + */ + private boolean registerKeyEventActivityListenerInternal(IKeyEventActivityListener listener) { + synchronized (mKeyEventActivityLock) { + if (!mKeyEventActivityListenersToNotify.contains(listener)) { + mKeyEventActivityListenersToNotify.add(listener); + return true; + } + } + return false; + } + + /** + * Unregisters a listener for updates to key event activeness + */ + private boolean unregisterKeyEventActivityListenerInternal(IKeyEventActivityListener listener) { + synchronized (mKeyEventActivityLock) { + return mKeyEventActivityListenersToNotify.removeIf(existingListener -> + existingListener.asBinder() == listener.asBinder()); + } + } + + private void notifyKeyActivityListeners(KeyEvent event) { + long currentTimeMs = SystemClock.uptimeMillis(); + if (keyEventActivityDetection() + && event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0 + && currentTimeMs - mLastKeyEventActivityTimeMs + >= KEY_EVENT_ACTIVITY_RATE_LIMIT_INTERVAL_MS) { + List<IKeyEventActivityListener> keyEventActivityListeners; + synchronized (mKeyEventActivityLock) { + keyEventActivityListeners = List.copyOf(mKeyEventActivityListenersToNotify); + } + for (IKeyEventActivityListener listener : keyEventActivityListeners) { + try { + listener.onKeyEventActivity(); + } catch (RemoteException e) { + Slog.i(TAG, + "Could Not Notify Listener due to Remote Exception: " + e); + unregisterKeyEventActivityListener(listener); + } + } + mLastKeyEventActivityTimeMs = currentTimeMs; + } + } + // Native callback. @SuppressWarnings("unused") - private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + @VisibleForTesting + public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + notifyKeyActivityListeners(event); synchronized (mFocusEventDebugViewLock) { if (mFocusEventDebugView != null) { mFocusEventDebugView.reportKeyEvent(event); diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 40f4f1ab0791..5259455cf33c 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -31,9 +31,12 @@ import android.hardware.input.InputManagerGlobal import android.hardware.input.InputSettings import android.hardware.input.KeyGestureEvent import android.os.InputEventInjectionSync +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.flag.junit.SetFlagsRule import android.provider.Settings import android.view.View.OnKeyListener @@ -66,11 +69,13 @@ import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.`when` import org.mockito.stubbing.OngoingStubbing @@ -136,10 +141,19 @@ class InputManagerServiceTests { private lateinit var testLooper: TestLooper private lateinit var contentResolver: MockContentResolver private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + private lateinit var fakePermissionEnforcer: FakePermissionEnforcer @Before fun setup() { context = spy(ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())) + fakePermissionEnforcer = FakePermissionEnforcer() + doReturn(Context.PERMISSION_ENFORCER_SERVICE).`when`(context).getSystemServiceName( + eq(PermissionEnforcer::class.java) + ) + doReturn(fakePermissionEnforcer).`when`(context).getSystemService( + eq(Context.PERMISSION_ENFORCER_SERVICE) + ) + contentResolver = MockContentResolver(context) contentResolver.addProvider(Settings.AUTHORITY, FakeSettingsProvider()) whenever(context.contentResolver).thenReturn(contentResolver) @@ -162,7 +176,7 @@ class InputManagerServiceTests { ): InputManagerService.KeyboardBacklightControllerInterface { return kbdController } - }) + }, fakePermissionEnforcer) inputManagerGlobalSession = InputManagerGlobal.createTestSession(service) val inputManager = InputManager(context) whenever(context.getSystemService(InputManager::class.java)).thenReturn(inputManager) @@ -314,6 +328,36 @@ class InputManagerServiceTests { } } + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_KEY_EVENT_ACTIVITY_DETECTION) + fun testKeyActivenessNotifyEventsLifecycle() { + service.systemRunning() + + fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY); + + val inputManager = context.getSystemService(InputManager::class.java) + + /* register for key event activeness */ + var listener = mock(InputManager.KeyEventActivityListener::class.java) + assertEquals(true, inputManager.registerKeyEventActivityListener(listener)) + + /* mimic key event pressed */ + val event = createKeycodeAEvent(createInputDevice(), KeyEvent.ACTION_DOWN) + service.interceptKeyBeforeQueueing(event, 0) + + /* verify onKeyEventActivity callback called */ + verify(listener, times(1)).onKeyEventActivity() + + /* unregister for key event activeness */ + assertEquals(true, inputManager.unregisterKeyEventActivityListener(listener)) + + /* mimic key event pressed */ + service.interceptKeyBeforeQueueing(event, /* policyFlags */ 0) + + /* verify onKeyEventActivity callback not called */ + verifyNoMoreInteractions(listener) + } + private class AutoClosingVirtualDisplays(val displays: List<VirtualDisplay>) : AutoCloseable { operator fun get(i: Int): VirtualDisplay = displays[i] |