Merge "Add API to register listener for Sticky modifier state" into main
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 6626baf..7bea9ae 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.IInputDeviceBatteryState;
import android.hardware.input.IKeyboardBacklightListener;
import android.hardware.input.IKeyboardBacklightState;
+import android.hardware.input.IStickyModifierStateListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.TouchCalibration;
import android.os.CombinedVibration;
@@ -241,4 +242,14 @@
void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener);
HostUsiVersion getHostUsiVersionFromDisplayConfig(int displayId);
+
+ @EnforcePermission("MONITOR_STICKY_MODIFIER_STATE")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)")
+ void registerStickyModifierStateListener(IStickyModifierStateListener listener);
+
+ @EnforcePermission("MONITOR_STICKY_MODIFIER_STATE")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)")
+ void unregisterStickyModifierStateListener(IStickyModifierStateListener listener);
}
diff --git a/core/java/android/hardware/input/IStickyModifierStateListener.aidl b/core/java/android/hardware/input/IStickyModifierStateListener.aidl
new file mode 100644
index 0000000..bd139ab
--- /dev/null
+++ b/core/java/android/hardware/input/IStickyModifierStateListener.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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 IStickyModifierStateListener {
+
+ /**
+ * Called when the sticky modifier state is changed when A11y Sticky keys feature is enabled
+ */
+ void onStickyModifierStateChanged(int modifierState, int lockedModifierState);
+}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index f941ad8..4ebbde7 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1297,6 +1297,42 @@
}
/**
+ * Registers a Sticky modifier state change listener to be notified about {@link
+ * StickyModifierState} changes.
+ *
+ * @param executor an executor on which the callback will be called
+ * @param listener the {@link StickyModifierStateListener}
+ * @throws IllegalArgumentException if {@code listener} has already been registered previously.
+ * @throws NullPointerException if {@code listener} or {@code executor} is null.
+ * @hide
+ * @see #unregisterStickyModifierStateListener(StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void registerStickyModifierStateListener(@NonNull Executor executor,
+ @NonNull StickyModifierStateListener listener) throws IllegalArgumentException {
+ if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+ return;
+ }
+ mGlobal.registerStickyModifierStateListener(executor, listener);
+ }
+
+ /**
+ * Unregisters a previously added Sticky modifier state change listener.
+ *
+ * @param listener the {@link StickyModifierStateListener}
+ * @hide
+ * @see #registerStickyModifierStateListener(Executor, StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void unregisterStickyModifierStateListener(
+ @NonNull StickyModifierStateListener listener) {
+ if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+ return;
+ }
+ mGlobal.unregisterStickyModifierStateListener(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.
@@ -1378,4 +1414,23 @@
void onKeyboardBacklightChanged(
int deviceId, @NonNull KeyboardBacklightState state, boolean isTriggeredByKeyPress);
}
+
+ /**
+ * A callback used to be notified about sticky modifier state changes when A11y Sticky keys
+ * feature is enabled.
+ *
+ * @see #registerStickyModifierStateListener(Executor, StickyModifierStateListener)
+ * @see #unregisterStickyModifierStateListener(StickyModifierStateListener)
+ * @hide
+ */
+ public interface StickyModifierStateListener {
+ /**
+ * Called when the sticky modifier state changes.
+ * This method will be called once after the listener is successfully registered to provide
+ * the initial modifier state.
+ *
+ * @param state the new sticky modifier state, never null.
+ */
+ void onStickyModifierStateChanged(@NonNull StickyModifierState state);
+ }
}
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index 24a6911..7c104a0 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -27,6 +27,7 @@
import android.hardware.input.InputManager.InputDeviceListener;
import android.hardware.input.InputManager.KeyboardBacklightListener;
import android.hardware.input.InputManager.OnTabletModeChangedListener;
+import android.hardware.input.InputManager.StickyModifierStateListener;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
@@ -52,6 +53,7 @@
import android.view.InputEvent;
import android.view.InputMonitor;
import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
import android.view.PointerIcon;
import com.android.internal.annotations.GuardedBy;
@@ -100,6 +102,14 @@
@GuardedBy("mKeyboardBacklightListenerLock")
@Nullable private IKeyboardBacklightListener mKeyboardBacklightListener;
+ private final Object mStickyModifierStateListenerLock = new Object();
+ @GuardedBy("mStickyModifierStateListenerLock")
+ @Nullable
+ private ArrayList<StickyModifierStateListenerDelegate> mStickyModifierStateListeners;
+ @GuardedBy("mStickyModifierStateListenerLock")
+ @Nullable
+ private IStickyModifierStateListener mStickyModifierStateListener;
+
// InputDeviceSensorManager gets notified synchronously from the binder thread when input
// devices change, so it must be synchronized with the input device listeners.
@GuardedBy("mInputDeviceListeners")
@@ -905,6 +915,158 @@
}
}
+ private static final class StickyModifierStateListenerDelegate {
+ final InputManager.StickyModifierStateListener mListener;
+ final Executor mExecutor;
+
+ StickyModifierStateListenerDelegate(StickyModifierStateListener listener,
+ Executor executor) {
+ mListener = listener;
+ mExecutor = executor;
+ }
+
+ void notifyStickyModifierStateChange(int modifierState, int lockedModifierState) {
+ mExecutor.execute(() ->
+ mListener.onStickyModifierStateChanged(
+ new LocalStickyModifierState(modifierState, lockedModifierState)));
+ }
+ }
+
+ private class LocalStickyModifierStateListener extends IStickyModifierStateListener.Stub {
+
+ @Override
+ public void onStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ synchronized (mStickyModifierStateListenerLock) {
+ if (mStickyModifierStateListeners == null) return;
+ final int numListeners = mStickyModifierStateListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ mStickyModifierStateListeners.get(i)
+ .notifyStickyModifierStateChange(modifierState, lockedModifierState);
+ }
+ }
+ }
+ }
+
+ // Implementation of the android.hardware.input.StickyModifierState interface used to report
+ // the sticky modifier state via the StickyModifierStateListener interfaces.
+ private static final class LocalStickyModifierState extends StickyModifierState {
+
+ private final int mModifierState;
+ private final int mLockedModifierState;
+
+ LocalStickyModifierState(int modifierState, int lockedModifierState) {
+ mModifierState = modifierState;
+ mLockedModifierState = lockedModifierState;
+ }
+
+ @Override
+ public boolean isShiftModifierOn() {
+ return (mModifierState & KeyEvent.META_SHIFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isShiftModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_SHIFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isCtrlModifierOn() {
+ return (mModifierState & KeyEvent.META_CTRL_ON) != 0;
+ }
+
+ @Override
+ public boolean isCtrlModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_CTRL_ON) != 0;
+ }
+
+ @Override
+ public boolean isMetaModifierOn() {
+ return (mModifierState & KeyEvent.META_META_ON) != 0;
+ }
+
+ @Override
+ public boolean isMetaModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_META_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltModifierOn() {
+ return (mModifierState & KeyEvent.META_ALT_LEFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_ALT_LEFT_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltGrModifierOn() {
+ return (mModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0;
+ }
+
+ @Override
+ public boolean isAltGrModifierLocked() {
+ return (mLockedModifierState & KeyEvent.META_ALT_RIGHT_ON) != 0;
+ }
+ }
+
+ /**
+ * @see InputManager#registerStickyModifierStateListener(Executor, StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ void registerStickyModifierStateListener(@NonNull Executor executor,
+ @NonNull StickyModifierStateListener listener) throws IllegalArgumentException {
+ Objects.requireNonNull(executor, "executor should not be null");
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mStickyModifierStateListenerLock) {
+ if (mStickyModifierStateListener == null) {
+ mStickyModifierStateListeners = new ArrayList<>();
+ mStickyModifierStateListener = new LocalStickyModifierStateListener();
+
+ try {
+ mIm.registerStickyModifierStateListener(mStickyModifierStateListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ final int numListeners = mStickyModifierStateListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ if (mStickyModifierStateListeners.get(i).mListener == listener) {
+ throw new IllegalArgumentException("Listener has already been registered!");
+ }
+ }
+ StickyModifierStateListenerDelegate delegate =
+ new StickyModifierStateListenerDelegate(listener, executor);
+ mStickyModifierStateListeners.add(delegate);
+ }
+ }
+
+ /**
+ * @see InputManager#unregisterStickyModifierStateListener(StickyModifierStateListener)
+ */
+ @RequiresPermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ void unregisterStickyModifierStateListener(
+ @NonNull StickyModifierStateListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ synchronized (mStickyModifierStateListenerLock) {
+ if (mStickyModifierStateListeners == null) {
+ return;
+ }
+ mStickyModifierStateListeners.removeIf((delegate) -> delegate.mListener == listener);
+ if (mStickyModifierStateListeners.isEmpty()) {
+ try {
+ mIm.unregisterStickyModifierStateListener(mStickyModifierStateListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mStickyModifierStateListeners = null;
+ mStickyModifierStateListener = null;
+ }
+ }
+ }
+
/**
* @see InputManager#getKeyboardLayoutsForInputDevice(InputDeviceIdentifier)
*/
diff --git a/core/java/android/hardware/input/StickyModifierState.java b/core/java/android/hardware/input/StickyModifierState.java
new file mode 100644
index 0000000..a3f7a0a
--- /dev/null
+++ b/core/java/android/hardware/input/StickyModifierState.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+
+/**
+ * The StickyModifierState class is a representation of a modifier state when A11y Sticky keys
+ * feature is enabled
+ *
+ * @hide
+ */
+public abstract class StickyModifierState {
+
+ /**
+ * Represents whether current sticky modifier state includes 'Shift' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Shift' modifier in
+ * its metaState.
+ *
+ * @return whether Shift modifier key is on.
+ */
+ public abstract boolean isShiftModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Shift' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Shift'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Shift' key again to clear the locked state.
+ *
+ * @return whether Shift modifier key is locked.
+ */
+ public abstract boolean isShiftModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Ctrl' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Ctrl' modifier in
+ * its metaState.
+ *
+ * @return whether Ctrl modifier key is on.
+ */
+ public abstract boolean isCtrlModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Ctrl' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Ctrl'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Ctrl' key again to clear the locked state.
+ *
+ * @return whether Ctrl modifier key is locked.
+ */
+ public abstract boolean isCtrlModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Meta' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Meta' modifier in
+ * its metaState.
+ *
+ * @return whether Meta modifier key is on.
+ */
+ public abstract boolean isMetaModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Meta' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Meta'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Meta' key again to clear the locked state.
+ *
+ * @return whether Meta modifier key is locked.
+ */
+ public abstract boolean isMetaModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Alt' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'Alt' modifier in
+ * its metaState.
+ *
+ * @return whether Alt modifier key is on.
+ */
+ public abstract boolean isAltModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'Alt' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'Alt'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'Alt' key again to clear the locked state.
+ *
+ * @return whether Alt modifier key is locked.
+ */
+ public abstract boolean isAltModifierLocked();
+
+ /**
+ * Represents whether current sticky modifier state includes 'AltGr' modifier.
+ * <p> If {@code true} the next {@link android.view.KeyEvent} will contain 'AltGr' modifier in
+ * its metaState.
+ *
+ * @return whether AltGr modifier key is on.
+ */
+ public abstract boolean isAltGrModifierOn();
+
+ /**
+ * Represents whether current sticky modifier state includes 'AltGr' modifier, and it is
+ * locked.
+ * <p> If {@code true} any subsequent {@link android.view.KeyEvent} will contain 'AltGr'
+ * modifier in its metaState and this state will remain sticky (will not be cleared), until
+ * user presses 'AltGr' key again to clear the locked state.
+ *
+ * @return whether AltGr modifier key is locked.
+ */
+ public abstract boolean isAltGrModifierLocked();
+}
+
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 232a36f..e65bfab 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7692,6 +7692,13 @@
<permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT"
android:protectionLevel="signature" />
+ <!-- Allows low-level access to monitor sticky modifier state changes when A11Y Sticky keys
+ feature is enabled.
+ <p>Not for use by third-party applications.
+ @hide -->
+ <permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE"
+ 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 087c525..36dac83 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -48,6 +48,7 @@
import android.hardware.input.IInputManager;
import android.hardware.input.IInputSensorEventListener;
import android.hardware.input.IKeyboardBacklightListener;
+import android.hardware.input.IStickyModifierStateListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
@@ -319,6 +320,9 @@
// Manages Keyboard backlight
private final KeyboardBacklightControllerInterface mKeyboardBacklightController;
+ // Manages Sticky modifier state
+ private final StickyModifierStateController mStickyModifierStateController;
+
// Manages Keyboard modifier keys remapping
private final KeyRemapper mKeyRemapper;
@@ -464,6 +468,7 @@
? new KeyboardBacklightController(mContext, mNative, mDataStore,
injector.getLooper(), injector.getUEventManager())
: new KeyboardBacklightControllerInterface() {};
+ mStickyModifierStateController = new StickyModifierStateController();
mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
mUseDevInputEventForAudioJack =
@@ -2827,6 +2832,33 @@
yPosition)).sendToTarget();
}
+ @Override
+ @EnforcePermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void registerStickyModifierStateListener(
+ @NonNull IStickyModifierStateListener listener) {
+ super.registerStickyModifierStateListener_enforcePermission();
+ Objects.requireNonNull(listener);
+ mStickyModifierStateController.registerStickyModifierStateListener(listener,
+ Binder.getCallingPid());
+ }
+
+ @Override
+ @EnforcePermission(Manifest.permission.MONITOR_STICKY_MODIFIER_STATE)
+ public void unregisterStickyModifierStateListener(
+ @NonNull IStickyModifierStateListener listener) {
+ super.unregisterStickyModifierStateListener_enforcePermission();
+ Objects.requireNonNull(listener);
+ mStickyModifierStateController.unregisterStickyModifierStateListener(listener,
+ Binder.getCallingPid());
+ }
+
+ // Native callback
+ @SuppressWarnings("unused")
+ void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ mStickyModifierStateController.notifyStickyModifierStateChanged(modifierState,
+ lockedModifierState);
+ }
+
// Native callback.
@SuppressWarnings("unused")
boolean isInputMethodConnectionActive() {
diff --git a/services/core/java/com/android/server/input/StickyModifierStateController.java b/services/core/java/com/android/server/input/StickyModifierStateController.java
new file mode 100644
index 0000000..5a22c10
--- /dev/null
+++ b/services/core/java/com/android/server/input/StickyModifierStateController.java
@@ -0,0 +1,133 @@
+/*
+ * 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.IStickyModifierStateListener;
+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 the sticky
+ * modifier state for A11y Sticky keys feature.
+ */
+final class StickyModifierStateController {
+
+ private static final String TAG = "ModifierStateController";
+
+ // To enable these logs, run:
+ // 'adb shell setprop log.tag.ModifierStateController DEBUG' (requires restart)
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // List of currently registered sticky modifier state listeners
+ @GuardedBy("mStickyModifierStateListenerRecords")
+ private final SparseArray<StickyModifierStateListenerRecord>
+ mStickyModifierStateListenerRecords = new SparseArray<>();
+
+ public void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ if (DEBUG) {
+ Slog.d(TAG, "Sticky modifier state changed, modifierState = " + modifierState
+ + ", lockedModifierState = " + lockedModifierState);
+ }
+
+ synchronized (mStickyModifierStateListenerRecords) {
+ for (int i = 0; i < mStickyModifierStateListenerRecords.size(); i++) {
+ mStickyModifierStateListenerRecords.valueAt(i).notifyStickyModifierStateChanged(
+ modifierState, lockedModifierState);
+ }
+ }
+ }
+
+ /** Register the sticky modifier state listener for a process. */
+ @BinderThread
+ public void registerStickyModifierStateListener(IStickyModifierStateListener listener,
+ int pid) {
+ synchronized (mStickyModifierStateListenerRecords) {
+ if (mStickyModifierStateListenerRecords.get(pid) != null) {
+ throw new IllegalStateException("The calling process has already registered "
+ + "a StickyModifierStateListener.");
+ }
+ StickyModifierStateListenerRecord record = new StickyModifierStateListenerRecord(pid,
+ listener);
+ try {
+ listener.asBinder().linkToDeath(record, 0);
+ } catch (RemoteException ex) {
+ throw new RuntimeException(ex);
+ }
+ mStickyModifierStateListenerRecords.put(pid, record);
+ }
+ }
+
+ /** Unregister the sticky modifier state listener for a process. */
+ @BinderThread
+ public void unregisterStickyModifierStateListener(IStickyModifierStateListener listener,
+ int pid) {
+ synchronized (mStickyModifierStateListenerRecords) {
+ StickyModifierStateListenerRecord record = mStickyModifierStateListenerRecords.get(pid);
+ if (record == null) {
+ throw new IllegalStateException("The calling process has no registered "
+ + "StickyModifierStateListener.");
+ }
+ if (record.mListener.asBinder() != listener.asBinder()) {
+ throw new IllegalStateException("The calling process has a different registered "
+ + "StickyModifierStateListener.");
+ }
+ record.mListener.asBinder().unlinkToDeath(record, 0);
+ mStickyModifierStateListenerRecords.remove(pid);
+ }
+ }
+
+ private void onStickyModifierStateListenerDied(int pid) {
+ synchronized (mStickyModifierStateListenerRecords) {
+ mStickyModifierStateListenerRecords.remove(pid);
+ }
+ }
+
+ // A record of a registered sticky modifier state listener from one process.
+ private class StickyModifierStateListenerRecord implements IBinder.DeathRecipient {
+ public final int mPid;
+ public final IStickyModifierStateListener mListener;
+
+ StickyModifierStateListenerRecord(int pid, IStickyModifierStateListener listener) {
+ mPid = pid;
+ mListener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ if (DEBUG) {
+ Slog.d(TAG, "Sticky modifier state listener for pid " + mPid + " died.");
+ }
+ onStickyModifierStateListenerDied(mPid);
+ }
+
+ public void notifyStickyModifierStateChanged(int modifierState, int lockedModifierState) {
+ try {
+ mListener.onStickyModifierStateChanged(modifierState, lockedModifierState);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify process " + mPid
+ + " that sticky modifier state changed, assuming it died.", ex);
+ binderDied();
+ }
+ }
+ }
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 9ba0a2a..afb0b20 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -114,6 +114,7 @@
jmethodID notifyFocusChanged;
jmethodID notifySensorEvent;
jmethodID notifySensorAccuracy;
+ jmethodID notifyStickyModifierStateChanged;
jmethodID notifyStylusGestureStarted;
jmethodID isInputMethodConnectionActive;
jmethodID notifyVibratorState;
@@ -270,7 +271,8 @@
class NativeInputManager : public virtual InputReaderPolicyInterface,
public virtual InputDispatcherPolicyInterface,
public virtual PointerControllerPolicyInterface,
- public virtual PointerChoreographerPolicyInterface {
+ public virtual PointerChoreographerPolicyInterface,
+ public virtual InputFilterPolicyInterface {
protected:
virtual ~NativeInputManager();
@@ -388,6 +390,10 @@
PointerControllerInterface::ControllerType type) override;
void notifyPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
+ /* --- InputFilterPolicyInterface implementation --- */
+ void notifyStickyModifierStateChanged(uint32_t modifierState,
+ uint32_t lockedModifierState) override;
+
private:
sp<InputManagerInterface> mInputManager;
@@ -477,7 +483,7 @@
mServiceObj = env->NewGlobalRef(serviceObj);
- InputManager* im = new InputManager(this, *this, *this);
+ InputManager* im = new InputManager(this, *this, *this, *this);
mInputManager = im;
defaultServiceManager()->addService(String16("inputflinger"), im);
}
@@ -806,6 +812,14 @@
checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged");
}
+void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState,
+ uint32_t lockedModifierState) {
+ JNIEnv* env = jniEnv();
+ env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyStickyModifierStateChanged,
+ modifierState, lockedModifierState);
+ checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged");
+}
+
sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(int displayId) {
JNIEnv* env = jniEnv();
jlong nativeSurfaceControlPtr =
@@ -2957,6 +2971,9 @@
GET_METHOD_ID(gServiceClassInfo.onPointerDisplayIdChanged, clazz, "onPointerDisplayIdChanged",
"(IFF)V");
+ GET_METHOD_ID(gServiceClassInfo.notifyStickyModifierStateChanged, clazz,
+ "notifyStickyModifierStateChanged", "(II)V");
+
GET_METHOD_ID(gServiceClassInfo.onPointerDownOutsideFocus, clazz,
"onPointerDownOutsideFocus", "(Landroid/os/IBinder;)V");
diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
new file mode 100644
index 0000000..e2b0c36
--- /dev/null
+++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.assertTrue
+import kotlin.test.fail
+
+/**
+ * Tests for [InputManager.StickyModifierStateListener].
+ *
+ * Build/Install/Run:
+ * atest InputTests:StickyModifierStateListenerTest
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner::class)
+class StickyModifierStateListenerTest {
+
+ @get:Rule
+ val rule = SetFlagsRule()
+
+ private val testLooper = TestLooper()
+ private val executor = HandlerExecutor(Handler(testLooper.looper))
+ private var registeredListener: IStickyModifierStateListener? = 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() {
+ // Enable Sticky keys feature
+ rule.enableFlags(com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG)
+ rule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_INPUT_FILTER_RUST_IMPL)
+
+ context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock)
+ inputManager = InputManager(context)
+ `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+ .thenReturn(inputManager)
+
+ // Handle sticky modifier state listener registration.
+ doAnswer {
+ val listener = it.getArgument(0) as IStickyModifierStateListener
+ if (registeredListener != null &&
+ registeredListener!!.asBinder() != listener.asBinder()) {
+ // There can only be one registered sticky modifier state listener per process.
+ fail("Trying to register a new listener when one already exists")
+ }
+ registeredListener = listener
+ null
+ }.`when`(iInputManagerMock).registerStickyModifierStateListener(any())
+
+ // Handle sticky modifier state listener being unregistered.
+ doAnswer {
+ val listener = it.getArgument(0) as IStickyModifierStateListener
+ if (registeredListener == null ||
+ registeredListener!!.asBinder() != listener.asBinder()) {
+ fail("Trying to unregister a listener that is not registered")
+ }
+ registeredListener = null
+ null
+ }.`when`(iInputManagerMock).unregisterStickyModifierStateListener(any())
+ }
+
+ @After
+ fun tearDown() {
+ if (this::inputManagerGlobalSession.isInitialized) {
+ inputManagerGlobalSession.close()
+ }
+ }
+
+ private fun notifyStickyModifierStateChanged(modifierState: Int, lockedModifierState: Int) {
+ registeredListener!!.onStickyModifierStateChanged(modifierState, lockedModifierState)
+ }
+
+ @Test
+ fun testListenerIsNotifiedOnModifierStateChanged() {
+ var callbackCount = 0
+
+ // Add a sticky modifier state listener
+ inputManager.registerStickyModifierStateListener(executor) {
+ callbackCount++
+ }
+
+ // Notifying sticky modifier state change will notify the listener.
+ notifyStickyModifierStateChanged(0, 0)
+ testLooper.dispatchNext()
+ assertEquals(1, callbackCount)
+ }
+
+ @Test
+ fun testListenerHasCorrectModifierStateNotified() {
+ // Add a sticky modifier state listener
+ inputManager.registerStickyModifierStateListener(executor) {
+ state: StickyModifierState ->
+ assertTrue(state.isAltModifierOn)
+ assertTrue(state.isAltModifierLocked)
+ assertTrue(state.isShiftModifierOn)
+ assertTrue(!state.isShiftModifierLocked)
+ assertTrue(!state.isCtrlModifierOn)
+ assertTrue(!state.isCtrlModifierLocked)
+ assertTrue(!state.isMetaModifierOn)
+ assertTrue(!state.isMetaModifierLocked)
+ assertTrue(!state.isAltGrModifierOn)
+ assertTrue(!state.isAltGrModifierLocked)
+ }
+
+ // Notifying sticky modifier state change will notify the listener.
+ notifyStickyModifierStateChanged(
+ KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON or
+ KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON,
+ KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON
+ )
+ testLooper.dispatchNext()
+ }
+
+ @Test
+ fun testAddingListenersRegistersInternalCallbackListener() {
+ // Set up two callbacks.
+ val callback1 = InputManager.StickyModifierStateListener {}
+ val callback2 = InputManager.StickyModifierStateListener {}
+
+ assertNull(registeredListener)
+
+ // Adding the listener should register the callback with InputManagerService.
+ inputManager.registerStickyModifierStateListener(executor, callback1)
+ assertNotNull(registeredListener)
+
+ // Adding another listener should not register new internal listener.
+ val currListener = registeredListener
+ inputManager.registerStickyModifierStateListener(executor, callback2)
+ assertEquals(currListener, registeredListener)
+ }
+
+ @Test
+ fun testRemovingListenersUnregistersInternalCallbackListener() {
+ // Set up two callbacks.
+ val callback1 = InputManager.StickyModifierStateListener {}
+ val callback2 = InputManager.StickyModifierStateListener {}
+
+ inputManager.registerStickyModifierStateListener(executor, callback1)
+ inputManager.registerStickyModifierStateListener(executor, callback2)
+
+ // Only removing all listeners should remove the internal callback
+ inputManager.unregisterStickyModifierStateListener(callback1)
+ assertNotNull(registeredListener)
+ inputManager.unregisterStickyModifierStateListener(callback2)
+ assertNull(registeredListener)
+ }
+
+ @Test
+ fun testMultipleListeners() {
+ // Set up two callbacks.
+ var callbackCount1 = 0
+ var callbackCount2 = 0
+ val callback1 = InputManager.StickyModifierStateListener { _ -> callbackCount1++ }
+ val callback2 = InputManager.StickyModifierStateListener { _ -> callbackCount2++ }
+
+ // Add both sticky modifier state listeners
+ inputManager.registerStickyModifierStateListener(executor, callback1)
+ inputManager.registerStickyModifierStateListener(executor, callback2)
+
+ // Notifying sticky modifier state change trigger the both callbacks.
+ notifyStickyModifierStateChanged(0, 0)
+ testLooper.dispatchAll()
+ assertEquals(1, callbackCount1)
+ assertEquals(1, callbackCount2)
+
+ inputManager.unregisterStickyModifierStateListener(callback2)
+ // Notifying sticky modifier state change should still trigger callback1 but not callback2.
+ notifyStickyModifierStateChanged(0, 0)
+ testLooper.dispatchAll()
+ assertEquals(2, callbackCount1)
+ assertEquals(1, callbackCount2)
+ }
+}