summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/input/IInputManager.aidl11
-rw-r--r--core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl27
-rw-r--r--core/java/android/hardware/input/InputManager.java47
-rw-r--r--core/java/android/hardware/input/InputManagerGlobal.java101
-rw-r--r--core/res/AndroidManifest.xml6
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java57
-rw-r--r--services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java137
-rw-r--r--tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt202
-rw-r--r--tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt96
9 files changed, 667 insertions, 17 deletions
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 1767d6438999..98e11375f077 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -25,6 +25,7 @@ import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.IInputDeviceBatteryState;
import android.hardware.input.IKeyboardBacklightListener;
import android.hardware.input.IKeyboardBacklightState;
+import android.hardware.input.IKeyboardSystemShortcutListener;
import android.hardware.input.IStickyModifierStateListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.KeyboardLayoutSelectionResult;
@@ -239,4 +240,14 @@ interface IInputManager {
void unregisterStickyModifierStateListener(IStickyModifierStateListener listener);
KeyGlyphMap getKeyGlyphMap(int deviceId);
+
+ @EnforcePermission("MONITOR_KEYBOARD_SYSTEM_SHORTCUTS")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)")
+ void registerKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener);
+
+ @EnforcePermission("MONITOR_KEYBOARD_SYSTEM_SHORTCUTS")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)")
+ void unregisterKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener);
}
diff --git a/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl b/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl
new file mode 100644
index 000000000000..8d44917845f4
--- /dev/null
+++ b/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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 IKeyboardSystemShortcutListener {
+
+ /**
+ * Called when the keyboard system shortcut is triggered.
+ */
+ void onKeyboardSystemShortcutTriggered(int deviceId, in int[] keycodes, int modifierState,
+ int shortcut);
+}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index d7952eb26f7e..6bc522b2b386 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1378,6 +1378,36 @@ public final class InputManager {
}
/**
+ * Registers a keyboard system shortcut listener for {@link KeyboardSystemShortcut} being
+ * triggered.
+ *
+ * @param executor an executor on which the callback will be called
+ * @param listener the {@link KeyboardSystemShortcutListener}
+ * @throws IllegalArgumentException if {@code listener} has already been registered previously.
+ * @throws NullPointerException if {@code listener} or {@code executor} is null.
+ * @hide
+ * @see #unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)
+ public void registerKeyboardSystemShortcutListener(@NonNull Executor executor,
+ @NonNull KeyboardSystemShortcutListener listener) throws IllegalArgumentException {
+ mGlobal.registerKeyboardSystemShortcutListener(executor, listener);
+ }
+
+ /**
+ * Unregisters a previously added keyboard system shortcut listener.
+ *
+ * @param listener the {@link KeyboardSystemShortcutListener}
+ * @hide
+ * @see #registerKeyboardSystemShortcutListener(Executor, KeyboardSystemShortcutListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)
+ public void unregisterKeyboardSystemShortcutListener(
+ @NonNull KeyboardSystemShortcutListener listener) {
+ mGlobal.unregisterKeyboardSystemShortcutListener(listener);
+ }
+
+ /**
* A callback used to be notified about battery state changes for an input device. The
* {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the
* listener is successfully registered to provide the initial battery state of the device.
@@ -1478,4 +1508,21 @@ public final class InputManager {
*/
void onStickyModifierStateChanged(@NonNull StickyModifierState state);
}
+
+ /**
+ * A callback used to be notified about keyboard system shortcuts being triggered.
+ *
+ * @see #registerKeyboardSystemShortcutListener(Executor, KeyboardSystemShortcutListener)
+ * @see #unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener)
+ * @hide
+ */
+ public interface KeyboardSystemShortcutListener {
+ /**
+ * Called when a keyboard system shortcut is triggered.
+ *
+ * @param systemShortcut the shortcut info about the shortcut that was triggered.
+ */
+ void onKeyboardSystemShortcutTriggered(int deviceId,
+ @NonNull KeyboardSystemShortcut systemShortcut);
+ }
}
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index 7b471806cfc1..f7fa5577a047 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.KeyboardBacklightListener;
+import android.hardware.input.InputManager.KeyboardSystemShortcutListener;
import android.hardware.input.InputManager.OnTabletModeChangedListener;
import android.hardware.input.InputManager.StickyModifierStateListener;
import android.hardware.lights.Light;
@@ -110,6 +111,14 @@ public final class InputManagerGlobal {
@Nullable
private IStickyModifierStateListener mStickyModifierStateListener;
+ private final Object mKeyboardSystemShortcutListenerLock = new Object();
+ @GuardedBy("mKeyboardSystemShortcutListenerLock")
+ @Nullable
+ private ArrayList<KeyboardSystemShortcutListenerDelegate> mKeyboardSystemShortcutListeners;
+ @GuardedBy("mKeyboardSystemShortcutListenerLock")
+ @Nullable
+ private IKeyboardSystemShortcutListener mKeyboardSystemShortcutListener;
+
// InputDeviceSensorManager gets notified synchronously from the binder thread when input
// devices change, so it must be synchronized with the input device listeners.
@GuardedBy("mInputDeviceListeners")
@@ -1055,6 +1064,98 @@ public final class InputManagerGlobal {
}
}
+ private static final class KeyboardSystemShortcutListenerDelegate {
+ final KeyboardSystemShortcutListener mListener;
+ final Executor mExecutor;
+
+ KeyboardSystemShortcutListenerDelegate(KeyboardSystemShortcutListener listener,
+ Executor executor) {
+ mListener = listener;
+ mExecutor = executor;
+ }
+
+ void onKeyboardSystemShortcutTriggered(int deviceId,
+ KeyboardSystemShortcut systemShortcut) {
+ mExecutor.execute(() ->
+ mListener.onKeyboardSystemShortcutTriggered(deviceId, systemShortcut));
+ }
+ }
+
+ private class LocalKeyboardSystemShortcutListener extends IKeyboardSystemShortcutListener.Stub {
+
+ @Override
+ public void onKeyboardSystemShortcutTriggered(int deviceId, int[] keycodes,
+ int modifierState, int shortcut) {
+ synchronized (mKeyboardSystemShortcutListenerLock) {
+ if (mKeyboardSystemShortcutListeners == null) return;
+ final int numListeners = mKeyboardSystemShortcutListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ mKeyboardSystemShortcutListeners.get(i)
+ .onKeyboardSystemShortcutTriggered(deviceId,
+ new KeyboardSystemShortcut(keycodes, modifierState, shortcut));
+ }
+ }
+ }
+ }
+
+ /**
+ * @see InputManager#registerKeyboardSystemShortcutListener(Executor,
+ * KeyboardSystemShortcutListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)
+ void registerKeyboardSystemShortcutListener(@NonNull Executor executor,
+ @NonNull KeyboardSystemShortcutListener listener) throws IllegalArgumentException {
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mKeyboardSystemShortcutListenerLock) {
+ if (mKeyboardSystemShortcutListener == null) {
+ mKeyboardSystemShortcutListeners = new ArrayList<>();
+ mKeyboardSystemShortcutListener = new LocalKeyboardSystemShortcutListener();
+
+ try {
+ mIm.registerKeyboardSystemShortcutListener(mKeyboardSystemShortcutListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ final int numListeners = mKeyboardSystemShortcutListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ if (mKeyboardSystemShortcutListeners.get(i).mListener == listener) {
+ throw new IllegalArgumentException("Listener has already been registered!");
+ }
+ }
+ KeyboardSystemShortcutListenerDelegate delegate =
+ new KeyboardSystemShortcutListenerDelegate(listener, executor);
+ mKeyboardSystemShortcutListeners.add(delegate);
+ }
+ }
+
+ /**
+ * @see InputManager#unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)
+ void unregisterKeyboardSystemShortcutListener(
+ @NonNull KeyboardSystemShortcutListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mKeyboardSystemShortcutListenerLock) {
+ if (mKeyboardSystemShortcutListeners == null) {
+ return;
+ }
+ mKeyboardSystemShortcutListeners.removeIf((delegate) -> delegate.mListener == listener);
+ if (mKeyboardSystemShortcutListeners.isEmpty()) {
+ try {
+ mIm.unregisterKeyboardSystemShortcutListener(mKeyboardSystemShortcutListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mKeyboardSystemShortcutListeners = null;
+ mKeyboardSystemShortcutListener = null;
+ }
+ }
+ }
+
/**
* TODO(b/330517633): Cleanup the unsupported API
*/
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a00cc8b91627..78bf6db4feb2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8131,6 +8131,12 @@
<permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE"
android:protectionLevel="signature" />
+ <!-- Allows low-level access to monitor keyboard system shortcuts
+ <p>Not for use by third-party applications.
+ @hide -->
+ <permission android:name="android.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS"
+ android:protectionLevel="signature" />
+
<uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
<!-- Allows financed device kiosk apps to perform actions on the Device Lock service
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 3a08767caa05..e555761e34e1 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -47,6 +47,7 @@ import android.hardware.input.IInputDevicesChangedListener;
import android.hardware.input.IInputManager;
import android.hardware.input.IInputSensorEventListener;
import android.hardware.input.IKeyboardBacklightListener;
+import android.hardware.input.IKeyboardSystemShortcutListener;
import android.hardware.input.IStickyModifierStateListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.InputDeviceIdentifier;
@@ -158,7 +159,7 @@ public class InputManagerService extends IInputManager.Stub
private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1;
private static final int MSG_RELOAD_DEVICE_ALIASES = 2;
private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 3;
- private static final int MSG_LOG_KEYBOARD_SYSTEM_SHORTCUT = 4;
+ private static final int MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED = 4;
private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
private static final AdditionalDisplayInputProperties
@@ -308,6 +309,9 @@ public class InputManagerService extends IInputManager.Stub
// Manages Sticky modifier state
private final StickyModifierStateController mStickyModifierStateController;
+ // Manages keyboard system shortcut callbacks
+ private final KeyboardShortcutCallbackHandler mKeyboardShortcutCallbackHandler;
+
// Manages Keyboard microphone mute led
private final KeyboardLedController mKeyboardLedController;
@@ -463,6 +467,7 @@ public class InputManagerService extends IInputManager.Stub
injector.getLooper(), injector.getUEventManager())
: new KeyboardBacklightControllerInterface() {};
mStickyModifierStateController = new StickyModifierStateController();
+ mKeyboardShortcutCallbackHandler = new KeyboardShortcutCallbackHandler();
mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(),
mNative);
mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
@@ -1180,11 +1185,6 @@ public class InputManagerService extends IInputManager.Stub
}
}
- private void logKeyboardSystemShortcut(int deviceId, KeyboardSystemShortcut shortcut) {
- mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_SHORTCUT, deviceId, 0,
- shortcut).sendToTarget();
- }
-
@Override // Binder call
public KeyboardLayout[] getKeyboardLayouts() {
return mKeyboardLayoutManager.getKeyboardLayouts();
@@ -2710,6 +2710,36 @@ public class InputManagerService extends IInputManager.Stub
lockedModifierState);
}
+ @Override
+ @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)
+ public void registerKeyboardSystemShortcutListener(
+ @NonNull IKeyboardSystemShortcutListener listener) {
+ super.registerKeyboardSystemShortcutListener_enforcePermission();
+ Objects.requireNonNull(listener);
+ mKeyboardShortcutCallbackHandler.registerKeyboardSystemShortcutListener(listener,
+ Binder.getCallingPid());
+ }
+
+ @Override
+ @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)
+ public void unregisterKeyboardSystemShortcutListener(
+ @NonNull IKeyboardSystemShortcutListener listener) {
+ super.unregisterKeyboardSystemShortcutListener_enforcePermission();
+ Objects.requireNonNull(listener);
+ mKeyboardShortcutCallbackHandler.unregisterKeyboardSystemShortcutListener(listener,
+ Binder.getCallingPid());
+ }
+
+ private void handleKeyboardSystemShortcutTriggered(int deviceId,
+ KeyboardSystemShortcut shortcut) {
+ InputDevice device = getInputDevice(deviceId);
+ if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
+ return;
+ }
+ KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(device, shortcut);
+ mKeyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(deviceId, shortcut);
+ }
+
/**
* Callback interface implemented by the Window Manager.
*/
@@ -2878,17 +2908,10 @@ public class InputManagerService extends IInputManager.Stub
boolean inTabletMode = (boolean) args.arg1;
deliverTabletModeChanged(whenNanos, inTabletMode);
break;
- case MSG_LOG_KEYBOARD_SYSTEM_SHORTCUT:
+ case MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED:
int deviceId = msg.arg1;
KeyboardSystemShortcut shortcut = (KeyboardSystemShortcut) msg.obj;
- InputDevice device = getInputDevice(deviceId);
- // Logging Keyboard system event only for an external HW keyboard. We should not
- // log events for virtual keyboards or internal Key events.
- if (device == null || device.isVirtual() || !device.isFullKeyboard()
- || !device.isExternal()) {
- return;
- }
- KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(device, shortcut);
+ handleKeyboardSystemShortcutTriggered(deviceId, shortcut);
}
}
}
@@ -3218,8 +3241,8 @@ public class InputManagerService extends IInputManager.Stub
@Override
public void notifyKeyboardShortcutTriggered(int deviceId, int[] keycodes, int modifierState,
@KeyboardSystemShortcut.SystemShortcut int shortcut) {
- logKeyboardSystemShortcut(deviceId,
- new KeyboardSystemShortcut(keycodes, modifierState, shortcut));
+ mHandler.obtainMessage(MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED, deviceId, 0,
+ new KeyboardSystemShortcut(keycodes, modifierState, shortcut)).sendToTarget();
}
}
diff --git a/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java b/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java
new file mode 100644
index 000000000000..092058e6f7d0
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 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 android.annotation.BinderThread;
+import android.hardware.input.IKeyboardSystemShortcutListener;
+import android.hardware.input.KeyboardSystemShortcut;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * A thread-safe component of {@link InputManagerService} responsible for managing callbacks when a
+ * keyboard shortcut is triggered.
+ */
+final class KeyboardShortcutCallbackHandler {
+
+ private static final String TAG = "KeyboardShortcut";
+
+ // To enable these logs, run:
+ // 'adb shell setprop log.tag.KeyboardShortcutCallbackHandler DEBUG' (requires restart)
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // List of currently registered keyboard system shortcut listeners keyed by process pid
+ @GuardedBy("mKeyboardSystemShortcutListenerRecords")
+ private final SparseArray<KeyboardSystemShortcutListenerRecord>
+ mKeyboardSystemShortcutListenerRecords = new SparseArray<>();
+
+ public void onKeyboardSystemShortcutTriggered(int deviceId,
+ KeyboardSystemShortcut systemShortcut) {
+ if (DEBUG) {
+ Slog.d(TAG, "Keyboard system shortcut triggered, deviceId = " + deviceId
+ + ", systemShortcut = " + systemShortcut);
+ }
+
+ synchronized (mKeyboardSystemShortcutListenerRecords) {
+ for (int i = 0; i < mKeyboardSystemShortcutListenerRecords.size(); i++) {
+ mKeyboardSystemShortcutListenerRecords.valueAt(i).onKeyboardSystemShortcutTriggered(
+ deviceId, systemShortcut);
+ }
+ }
+ }
+
+ /** Register the keyboard system shortcut listener for a process. */
+ @BinderThread
+ public void registerKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener,
+ int pid) {
+ synchronized (mKeyboardSystemShortcutListenerRecords) {
+ if (mKeyboardSystemShortcutListenerRecords.get(pid) != null) {
+ throw new IllegalStateException("The calling process has already registered "
+ + "a KeyboardSystemShortcutListener.");
+ }
+ KeyboardSystemShortcutListenerRecord record = new KeyboardSystemShortcutListenerRecord(
+ pid, listener);
+ try {
+ listener.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException(ex);
+ }
+ mKeyboardSystemShortcutListenerRecords.put(pid, record);
+ }
+ }
+
+ /** Unregister the keyboard system shortcut listener for a process. */
+ @BinderThread
+ public void unregisterKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener,
+ int pid) {
+ synchronized (mKeyboardSystemShortcutListenerRecords) {
+ KeyboardSystemShortcutListenerRecord record =
+ mKeyboardSystemShortcutListenerRecords.get(pid);
+ if (record == null) {
+ throw new IllegalStateException("The calling process has no registered "
+ + "KeyboardSystemShortcutListener.");
+ }
+ if (record.mListener.asBinder() != listener.asBinder()) {
+ throw new IllegalStateException("The calling process has a different registered "
+ + "KeyboardSystemShortcutListener.");
+ }
+ record.mListener.asBinder().unlinkToDeath(record, 0);
+ mKeyboardSystemShortcutListenerRecords.remove(pid);
+ }
+ }
+
+ private void onKeyboardSystemShortcutListenerDied(int pid) {
+ synchronized (mKeyboardSystemShortcutListenerRecords) {
+ mKeyboardSystemShortcutListenerRecords.remove(pid);
+ }
+ }
+
+ // A record of a registered keyboard system shortcut listener from one process.
+ private class KeyboardSystemShortcutListenerRecord implements IBinder.DeathRecipient {
+ public final int mPid;
+ public final IKeyboardSystemShortcutListener mListener;
+
+ KeyboardSystemShortcutListenerRecord(int pid, IKeyboardSystemShortcutListener listener) {
+ mPid = pid;
+ mListener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ if (DEBUG) {
+ Slog.d(TAG, "Keyboard system shortcut listener for pid " + mPid + " died.");
+ }
+ onKeyboardSystemShortcutListenerDied(mPid);
+ }
+
+ public void onKeyboardSystemShortcutTriggered(int deviceId, KeyboardSystemShortcut data) {
+ try {
+ mListener.onKeyboardSystemShortcutTriggered(deviceId, data.getKeycodes(),
+ data.getModifierState(), data.getSystemShortcut());
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid
+ + " that keyboard system shortcut was triggered, assuming it died.", ex);
+ binderDied();
+ }
+ }
+ }
+}
diff --git a/tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt b/tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt
new file mode 100644
index 000000000000..24d7291bec87
--- /dev/null
+++ b/tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2024 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
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Handler
+import android.os.HandlerExecutor
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import com.android.server.testutils.any
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnitRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.fail
+
+/**
+ * Tests for [InputManager.KeyboardSystemShortcutListener].
+ *
+ * Build/Install/Run:
+ * atest InputTests:KeyboardSystemShortcutListenerTest
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner::class)
+class KeyboardSystemShortcutListenerTest {
+
+ companion object {
+ const val DEVICE_ID = 1
+ val HOME_SHORTCUT = KeyboardSystemShortcut(
+ intArrayOf(KeyEvent.KEYCODE_H),
+ KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON,
+ KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME
+ )
+ }
+
+ @get:Rule
+ val rule = SetFlagsRule()
+
+ private val testLooper = TestLooper()
+ private val executor = HandlerExecutor(Handler(testLooper.looper))
+ private var registeredListener: IKeyboardSystemShortcutListener? = null
+ private lateinit var context: Context
+ private lateinit var inputManager: InputManager
+ private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
+
+ @Mock
+ private lateinit var iInputManagerMock: IInputManager
+
+ @Before
+ fun setUp() {
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock)
+ inputManager = InputManager(context)
+ `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+
+ // Handle keyboard system shortcut listener registration.
+ doAnswer {
+ val listener = it.getArgument(0) as IKeyboardSystemShortcutListener
+ if (registeredListener != null &&
+ registeredListener!!.asBinder() != listener.asBinder()) {
+ // There can only be one registered keyboard system shortcut listener per process.
+ fail("Trying to register a new listener when one already exists")
+ }
+ registeredListener = listener
+ null
+ }.`when`(iInputManagerMock).registerKeyboardSystemShortcutListener(any())
+
+ // Handle keyboard system shortcut listener being unregistered.
+ doAnswer {
+ val listener = it.getArgument(0) as IKeyboardSystemShortcutListener
+ if (registeredListener == null ||
+ registeredListener!!.asBinder() != listener.asBinder()) {
+ fail("Trying to unregister a listener that is not registered")
+ }
+ registeredListener = null
+ null
+ }.`when`(iInputManagerMock).unregisterKeyboardSystemShortcutListener(any())
+ }
+
+ @After
+ fun tearDown() {
+ if (this::inputManagerGlobalSession.isInitialized) {
+ inputManagerGlobalSession.close()
+ }
+ }
+
+ private fun notifyKeyboardSystemShortcutTriggered(id: Int, shortcut: KeyboardSystemShortcut) {
+ registeredListener!!.onKeyboardSystemShortcutTriggered(
+ id,
+ shortcut.keycodes,
+ shortcut.modifierState,
+ shortcut.systemShortcut
+ )
+ }
+
+ @Test
+ fun testListenerHasCorrectSystemShortcutNotified() {
+ var callbackCount = 0
+
+ // Add a keyboard system shortcut listener
+ inputManager.registerKeyboardSystemShortcutListener(executor) {
+ deviceId: Int, systemShortcut: KeyboardSystemShortcut ->
+ assertEquals(DEVICE_ID, deviceId)
+ assertEquals(HOME_SHORTCUT, systemShortcut)
+ callbackCount++
+ }
+
+ // Notifying keyboard system shortcut triggered will notify the listener.
+ notifyKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT)
+ testLooper.dispatchNext()
+ assertEquals(1, callbackCount)
+ }
+
+ @Test
+ fun testAddingListenersRegistersInternalCallbackListener() {
+ // Set up two callbacks.
+ val callback1 = InputManager.KeyboardSystemShortcutListener {_, _ -> }
+ val callback2 = InputManager.KeyboardSystemShortcutListener {_, _ -> }
+
+ assertNull(registeredListener)
+
+ // Adding the listener should register the callback with InputManagerService.
+ inputManager.registerKeyboardSystemShortcutListener(executor, callback1)
+ assertNotNull(registeredListener)
+
+ // Adding another listener should not register new internal listener.
+ val currListener = registeredListener
+ inputManager.registerKeyboardSystemShortcutListener(executor, callback2)
+ assertEquals(currListener, registeredListener)
+ }
+
+ @Test
+ fun testRemovingListenersUnregistersInternalCallbackListener() {
+ // Set up two callbacks.
+ val callback1 = InputManager.KeyboardSystemShortcutListener {_, _ -> }
+ val callback2 = InputManager.KeyboardSystemShortcutListener {_, _ -> }
+
+ inputManager.registerKeyboardSystemShortcutListener(executor, callback1)
+ inputManager.registerKeyboardSystemShortcutListener(executor, callback2)
+
+ // Only removing all listeners should remove the internal callback
+ inputManager.unregisterKeyboardSystemShortcutListener(callback1)
+ assertNotNull(registeredListener)
+ inputManager.unregisterKeyboardSystemShortcutListener(callback2)
+ assertNull(registeredListener)
+ }
+
+ @Test
+ fun testMultipleListeners() {
+ // Set up two callbacks.
+ var callbackCount1 = 0
+ var callbackCount2 = 0
+ val callback1 = InputManager.KeyboardSystemShortcutListener { _, _ -> callbackCount1++ }
+ val callback2 = InputManager.KeyboardSystemShortcutListener { _, _ -> callbackCount2++ }
+
+ // Add both keyboard system shortcut listeners
+ inputManager.registerKeyboardSystemShortcutListener(executor, callback1)
+ inputManager.registerKeyboardSystemShortcutListener(executor, callback2)
+
+ // Notifying keyboard system shortcut triggered, should notify both the callbacks.
+ notifyKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT)
+ testLooper.dispatchAll()
+ assertEquals(1, callbackCount1)
+ assertEquals(1, callbackCount2)
+
+ inputManager.unregisterKeyboardSystemShortcutListener(callback2)
+ // Notifying keyboard system shortcut triggered, should still trigger callback1 but not
+ // callback2.
+ notifyKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT)
+ testLooper.dispatchAll()
+ assertEquals(2, callbackCount1)
+ assertEquals(1, callbackCount2)
+ }
+}
diff --git a/tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt b/tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt
new file mode 100644
index 000000000000..5a40a1c8201e
--- /dev/null
+++ b/tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 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 android.content.Context
+import android.content.ContextWrapper
+import android.hardware.input.IKeyboardSystemShortcutListener
+import android.hardware.input.KeyboardSystemShortcut
+import android.platform.test.annotations.Presubmit
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+
+/**
+ * Tests for {@link KeyboardShortcutCallbackHandler}.
+ *
+ * Build/Install/Run:
+ * atest InputTests:KeyboardShortcutCallbackHandlerTests
+ */
+@Presubmit
+class KeyboardShortcutCallbackHandlerTests {
+
+ companion object {
+ val DEVICE_ID = 1
+ val HOME_SHORTCUT = KeyboardSystemShortcut(
+ intArrayOf(KeyEvent.KEYCODE_H),
+ KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON,
+ KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME
+ )
+ }
+
+ @get:Rule
+ val rule = MockitoJUnit.rule()!!
+
+ private lateinit var keyboardShortcutCallbackHandler: KeyboardShortcutCallbackHandler
+ private lateinit var context: Context
+ private var lastShortcut: KeyboardSystemShortcut? = null
+
+ @Before
+ fun setup() {
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ keyboardShortcutCallbackHandler = KeyboardShortcutCallbackHandler()
+ }
+
+ @Test
+ fun testKeyboardSystemShortcutTriggered_registerUnregisterListener() {
+ val listener = KeyboardSystemShortcutListener()
+
+ // Register keyboard system shortcut listener
+ keyboardShortcutCallbackHandler.registerKeyboardSystemShortcutListener(listener, 0)
+ keyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT)
+ assertEquals(
+ "Listener should get callback on keyboard system shortcut triggered",
+ HOME_SHORTCUT,
+ lastShortcut!!
+ )
+
+ // Unregister listener
+ lastShortcut = null
+ keyboardShortcutCallbackHandler.unregisterKeyboardSystemShortcutListener(listener, 0)
+ keyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT)
+ assertNull("Listener should not get callback after being unregistered", lastShortcut)
+ }
+
+ inner class KeyboardSystemShortcutListener : IKeyboardSystemShortcutListener.Stub() {
+ override fun onKeyboardSystemShortcutTriggered(
+ deviceId: Int,
+ keycodes: IntArray,
+ modifierState: Int,
+ shortcut: Int
+ ) {
+ assertEquals(DEVICE_ID, deviceId)
+ lastShortcut = KeyboardSystemShortcut(keycodes, modifierState, shortcut)
+ }
+ }
+} \ No newline at end of file