summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/input/IInputManager.aidl11
-rw-r--r--core/java/android/hardware/input/IKeyEventActivityListener.aidl23
-rw-r--r--core/java/android/hardware/input/InputManager.java37
-rw-r--r--core/java/android/hardware/input/InputManagerGlobal.java65
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java87
-rw-r--r--tests/Input/src/com/android/server/input/InputManagerServiceTests.kt46
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]