Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e595259b authored by Prabir Pradhan's avatar Prabir Pradhan
Browse files

InputManagerService: Refresh device sysfs node when leds/battery added

The sysfs leds or power_supply nodes for a peripheral device are added
asynchronously relative to the evdev nodes. Therefore, we cannot use the
addition of an evdev node as an assumption that the peripheral has been
completely configured in the kernel. There could still be sysfs nodes
for leds or battery, for example, that are added later.

To account for these cases, we need to monitor for any UEvents sent by
the kernel to tell when any of these nodes are added.

In this CL, we create a new InputManagerService component called
SysfsNodeMonitor that watches for device additions. When a new external
device is added, it will start monitoring for UEvents for that device
for at most one minute, or until the device is disconnected. After
registering the UEvent observer, we force the watched sysfs node to
refresh to catch any UEvents that were potentially missed before the
observer was added.

The observer will look out for leds or power_supply node additions, and
will notify native code when it detects node additions.

For now, such a notification will cause the sysfs root node to be
refreshed in EventHub to look for any diffs in the recognized device
properties.

A change in the device properties will cause the input device to be
removed and immediately reopened with a new deviceId. This is a change
in behavior from before, where the immediate removal and re-addition
would not occur.

Bug: 397208968
Test: Presubmit
Flag: EXEMPT bug fix
Change-Id: Id982e6550cd49cdc52e53f1f0dd175068b5bd8a4
parent 5434639a
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -343,6 +343,9 @@ public class InputManagerService extends IInputManager.Stub
    // Manages battery state for input devices.
    private final BatteryController mBatteryController;

    // Monitors any changes to the sysfs nodes when an input device is connected.
    private final SysfsNodeMonitor mSysfsNodeMonitor;

    @Nullable
    private final TouchpadDebugViewController mTouchpadDebugViewController;

@@ -535,6 +538,8 @@ public class InputManagerService extends IInputManager.Stub
                        injector.getLooper(), this) : null;
        mBatteryController = new BatteryController(mContext, mNative, injector.getLooper(),
                injector.getUEventManager());
        mSysfsNodeMonitor = new SysfsNodeMonitor(mContext, mNative, injector.getLooper(),
                injector.getUEventManager());
        mKeyboardBacklightController = injector.getKeyboardBacklightController(mNative);
        mStickyModifierStateController = new StickyModifierStateController();
        mInputDataStore = new InputDataStore();
@@ -664,6 +669,7 @@ public class InputManagerService extends IInputManager.Stub

        mKeyboardLayoutManager.systemRunning();
        mBatteryController.systemRunning();
        mSysfsNodeMonitor.systemRunning();
        mKeyboardBacklightController.systemRunning();
        mKeyboardLedController.systemRunning();
        mKeyRemapper.systemRunning();
+6 −0
Original line number Diff line number Diff line
@@ -272,6 +272,9 @@ interface NativeInputManagerService {
    /** Set whether showing a pointer icon for styluses is enabled. */
    void setStylusPointerIconEnabled(boolean enabled);

    /** Get the sysfs root path of an input device if known, otherwise return null. */
    @Nullable String getSysfsRootPath(int deviceId);

    /**
     * Report sysfs node changes. This may result in recreation of the corresponding InputDevice.
     * The recreated device may contain new associated peripheral devices like Light, Battery, etc.
@@ -618,6 +621,9 @@ interface NativeInputManagerService {
        @Override
        public native void setStylusPointerIconEnabled(boolean enabled);

        @Override
        public native String getSysfsRootPath(int deviceId);

        @Override
        public native void sysfsNodeChanged(String sysfsNodePath);

+203 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.server.input;

import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.Looper;
import android.os.UEventObserver;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;

import java.util.Objects;

/**
 * A thread-safe component of {@link InputManagerService} responsible for monitoring the addition
 * of kernel sysfs nodes for newly connected input devices.
 *
 * This class uses the {@link UEventObserver} to monitor for changes to an input device's sysfs
 * nodes, and is responsible for requesting the native code to refresh its sysfs nodes when there
 * is a change. This is necessary because the sysfs nodes may only be configured after an input
 * device is already added, with no way for the native code to detect any changes afterwards.
 */
final class SysfsNodeMonitor {
    private static final String TAG = SysfsNodeMonitor.class.getSimpleName();

    // To enable these logs, run:
    // 'adb shell setprop log.tag.SysfsNodeMonitor DEBUG' (requires restart)
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final long SYSFS_NODE_MONITORING_TIMEOUT_MS = 60_000; // 1 minute

    private final Context mContext;
    private final NativeInputManagerService mNative;
    private final Handler mHandler;
    private final UEventManager mUEventManager;

    private InputManager mInputManager;

    private final SparseArray<SysfsNodeAddedListener> mUEventListenersByDeviceId =
            new SparseArray<>();

    SysfsNodeMonitor(Context context, NativeInputManagerService nativeService, Looper looper,
            UEventManager uEventManager) {
        mContext = context;
        mNative = nativeService;
        mHandler = new Handler(looper);
        mUEventManager = uEventManager;
    }

    public void systemRunning() {
        mInputManager = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
        mInputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
        for (int deviceId : mInputManager.getInputDeviceIds()) {
            mInputDeviceListener.onInputDeviceAdded(deviceId);
        }
    }

    private final InputManager.InputDeviceListener mInputDeviceListener =
            new InputManager.InputDeviceListener() {
                @Override
                public void onInputDeviceAdded(int deviceId) {
                    startMonitoring(deviceId);
                }

                @Override
                public void onInputDeviceRemoved(int deviceId) {
                    stopMonitoring(deviceId);
                }

                @Override
                public void onInputDeviceChanged(int deviceId) {
                }
            };

    private void startMonitoring(int deviceId) {
        final var inputDevice = mInputManager.getInputDevice(deviceId);
        if (inputDevice == null) {
            return;
        }
        if (!inputDevice.isExternal()) {
            if (DEBUG) {
                Log.d(TAG, "Not listening to sysfs node changes for internal input device: "
                        + deviceId);
            }
            return;
        }
        final var sysfsRootPath = formatDevPath(mNative.getSysfsRootPath(deviceId));
        if (sysfsRootPath == null) {
            if (DEBUG) {
                Log.d(TAG, "Sysfs node not found for external input device: " + deviceId);
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "Start listening to sysfs node changes for input device: " + deviceId
                    + ", node: " + sysfsRootPath);
        }
        final var listener = new SysfsNodeAddedListener();
        mUEventListenersByDeviceId.put(deviceId, listener);

        // We must synchronously start monitoring for changes to this device's path.
        // Once monitoring starts, we need to trigger a native refresh of the sysfs nodes to
        // catch any changes that happened between the input device's creation and the UEvent
        // listener being added.
        // NOTE: This relies on the fact that the following `addListener` call is fully synchronous.
        mUEventManager.addListener(listener, sysfsRootPath);
        mNative.sysfsNodeChanged(sysfsRootPath);

        // Always stop listening for new sysfs nodes after the timeout.
        mHandler.postDelayed(() -> stopMonitoring(deviceId), SYSFS_NODE_MONITORING_TIMEOUT_MS);
    }

    private static String formatDevPath(String path) {
        // Remove the "/sys" prefix if it has one.
        return path != null && path.startsWith("/sys") ? path.substring(4) : path;
    }

    private void stopMonitoring(int deviceId) {
        final var listener = mUEventListenersByDeviceId.removeReturnOld(deviceId);
        if (listener == null) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "Stop listening to sysfs node changes for input device: " + deviceId);
        }
        mUEventManager.removeListener(listener);
    }

    class SysfsNodeAddedListener extends UEventManager.UEventListener {

        private boolean mHasReceivedRemovalNotification = false;
        private boolean mHasReceivedPowerSupplyNotification = false;

        @Override
        public void onUEvent(UEventObserver.UEvent event) {
            // This callback happens on the UEventObserver's thread.
            // Ensure we are processing on the handler thread.
            mHandler.post(() -> handleUEvent(event));
        }

        private void handleUEvent(UEventObserver.UEvent event) {
            if (DEBUG) {
                Slog.d(TAG, "UEventListener: Received UEvent: " + event);
            }
            final var subsystem = event.get("SUBSYSTEM");
            final var devPath = "/sys" + Objects.requireNonNull(
                    TextUtils.nullIfEmpty(event.get("DEVPATH")));
            final var action = event.get("ACTION");

            // NOTE: We must be careful to avoid reconfiguring sysfs nodes during device removal,
            // because it might result in the device getting re-opened in native code during
            // removal, resulting in unexpected states. If we see any removal action for this node,
            // ensure we stop responding altogether.
            if (mHasReceivedRemovalNotification || "REMOVE".equalsIgnoreCase(action)) {
                mHasReceivedRemovalNotification = true;
                return;
            }

            if ("LEDS".equalsIgnoreCase(subsystem) && "ADD".equalsIgnoreCase(action)) {
                // An LED node was added. Notify native code to reconfigure the sysfs node.
                if (DEBUG) {
                    Slog.d(TAG,
                            "Reconfiguring sysfs node because 'leds' node was added: " + devPath);
                }
                mNative.sysfsNodeChanged(devPath);
                return;
            }

            if ("POWER_SUPPLY".equalsIgnoreCase(subsystem)) {
                if (mHasReceivedPowerSupplyNotification) {
                    return;
                }
                // This is the first notification we received from the power_supply subsystem.
                // Notify native code that the battery node may have been added. The power_supply
                // subsystem does not seem to be sending ADD events, so use use the first event
                // with any action as a proxy for a new power_supply node being created.
                if (DEBUG) {
                    Slog.d(TAG, "Reconfiguring sysfs node because 'power_supply' node had action '"
                            + action + "': " + devPath);
                }
                mHasReceivedPowerSupplyNotification = true;
                mNative.sysfsNodeChanged(devPath);
            }
        }
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -2920,6 +2920,12 @@ static void nativeReloadDeviceAliases(JNIEnv* env, jobject nativeImplObj) {
            InputReaderConfiguration::Change::DEVICE_ALIAS);
}

static jstring nativeGetSysfsRootPath(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
    const auto path = im->getInputManager()->getReader().getSysfsRootPath(deviceId);
    return path.empty() ? nullptr : env->NewStringUTF(path.c_str());
}

static void nativeSysfsNodeChanged(JNIEnv* env, jobject nativeImplObj, jstring path) {
    ScopedUtfChars sysfsNodePathChars(env, path);
    const std::string sysfsNodePath = sysfsNodePathChars.c_str();
@@ -3382,6 +3388,7 @@ static const JNINativeMethod gInputManagerMethods[] = {
        {"getBatteryDevicePath", "(I)Ljava/lang/String;", (void*)nativeGetBatteryDevicePath},
        {"reloadKeyboardLayouts", "()V", (void*)nativeReloadKeyboardLayouts},
        {"reloadDeviceAliases", "()V", (void*)nativeReloadDeviceAliases},
        {"getSysfsRootPath", "(I)Ljava/lang/String;", (void*)nativeGetSysfsRootPath},
        {"sysfsNodeChanged", "(Ljava/lang/String;)V", (void*)nativeSysfsNodeChanged},
        {"dump", "()Ljava/lang/String;", (void*)nativeDump},
        {"monitor", "()V", (void*)nativeMonitor},