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

Commit df642e79 authored by Prabir Pradhan's avatar Prabir Pradhan Committed by Android (Google) Code Review
Browse files

Merge "Update monitored InputDevice battery state on an UEvent notification"

parents bac52cf5 e4f97779
Loading
Loading
Loading
Loading
+218 −11
Original line number Diff line number Diff line
@@ -18,12 +18,17 @@ package com.android.server.input;

import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UEventObserver;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -31,6 +36,8 @@ import android.util.Slog;
import android.view.InputDevice;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.input.BatteryController.UEventManager.UEventListener;

import java.io.PrintWriter;
import java.util.Arrays;
@@ -41,7 +48,7 @@ import java.util.Set;
 * A thread-safe component of {@link InputManagerService} responsible for managing the battery state
 * of input devices.
 */
final class BatteryController {
final class BatteryController implements InputManager.InputDeviceListener {
    private static final String TAG = BatteryController.class.getSimpleName();

    // To enable these logs, run:
@@ -51,15 +58,35 @@ final class BatteryController {
    private final Object mLock = new Object();
    private final Context mContext;
    private final NativeInputManagerService mNative;
    private final Handler mHandler;
    private final UEventManager mUEventManager;

    // Maps a pid to the registered listener record for that process. There can only be one battery
    // listener per process.
    @GuardedBy("mLock")
    private final ArrayMap<Integer, ListenerRecord> mListenerRecords = new ArrayMap<>();

    BatteryController(Context context, NativeInputManagerService nativeService) {
    // Maps a deviceId that is being monitored to the battery state for the device.
    // This must be kept in sync with {@link #mListenerRecords}.
    @GuardedBy("mLock")
    private final ArrayMap<Integer, MonitoredDeviceState> mMonitoredDeviceStates = new ArrayMap<>();

    BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) {
        this(context, nativeService, looper, new UEventManager() {});
    }

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

    void systemRunning() {
        Objects.requireNonNull(mContext.getSystemService(InputManager.class))
                .registerInputDeviceListener(this, mHandler);
    }

    /**
@@ -96,29 +123,45 @@ final class BatteryController {
                                + " is already monitoring deviceId " + deviceId);
            }

            MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId);
            if (deviceState == null) {
                // This is the first listener that is monitoring this device.
                deviceState = new MonitoredDeviceState(deviceId);
                mMonitoredDeviceStates.put(deviceId, deviceState);
            }

            if (DEBUG) {
                Slog.d(TAG, "Battery listener for pid " + pid
                        + " is monitoring deviceId " + deviceId);
            }

            notifyBatteryListener(deviceId, listenerRecord);
            notifyBatteryListener(listenerRecord, deviceState);
        }
    }

    private void notifyBatteryListener(int deviceId, ListenerRecord record) {
        final long eventTime = SystemClock.uptimeMillis();
    private static void notifyBatteryListener(ListenerRecord listenerRecord,
            MonitoredDeviceState deviceState) {
        try {
            record.mListener.onBatteryStateChanged(
                    deviceId,
                    hasBattery(deviceId),
                    mNative.getBatteryStatus(deviceId),
                    mNative.getBatteryCapacity(deviceId) / 100.f,
                    eventTime);
            listenerRecord.mListener.onBatteryStateChanged(
                    deviceState.mDeviceId,
                    deviceState.mHasBattery,
                    deviceState.mBatteryStatus,
                    deviceState.mBatteryCapacity,
                    deviceState.mLastUpdateTime);
        } catch (RemoteException e) {
            Slog.e(TAG, "Failed to notify listener", e);
        }
    }

    @GuardedBy("mLock")
    private void notifyAllListenersForDeviceLocked(MonitoredDeviceState deviceState) {
        mListenerRecords.forEach((pid, listenerRecord) -> {
            if (listenerRecord.mMonitoredDevices.contains(deviceState.mDeviceId)) {
                notifyBatteryListener(listenerRecord, deviceState);
            }
        });
    }

    private boolean hasBattery(int deviceId) {
        final InputDevice device =
                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -126,6 +169,12 @@ final class BatteryController {
        return device != null && device.hasBattery();
    }

    @GuardedBy("mLock")
    private MonitoredDeviceState getDeviceStateOrThrowLocked(int deviceId) {
        return Objects.requireNonNull(mMonitoredDeviceStates.get(deviceId),
                "Maps are out of sync: Cannot find device state for deviceId " + deviceId);
    }

    /**
     * Unregister the battery listener for the given input device and stop monitoring its battery
     * state. If there are no other input devices that this listener is monitoring, the listener is
@@ -170,6 +219,13 @@ final class BatteryController {
                    + pid);
        }

        if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
            // There are no more listeners monitoring this device.
            final MonitoredDeviceState deviceState = getDeviceStateOrThrowLocked(deviceId);
            deviceState.stopMonitoring();
            mMonitoredDeviceStates.remove(deviceId);
        }

        if (listenerRecord.mMonitoredDevices.isEmpty()) {
            // There are no more devices being monitored by this listener.
            listenerRecord.mListener.asBinder().unlinkToDeath(listenerRecord.mDeathRecipient, 0);
@@ -178,6 +234,16 @@ final class BatteryController {
        }
    }

    @GuardedBy("mLock")
    private boolean hasRegisteredListenerForDeviceLocked(int deviceId) {
        for (int i = 0; i < mListenerRecords.size(); i++) {
            if (mListenerRecords.valueAt(i).mMonitoredDevices.contains(deviceId)) {
                return true;
            }
        }
        return false;
    }

    private void handleListeningProcessDied(int pid) {
        synchronized (mLock) {
            final ListenerRecord listenerRecord = mListenerRecords.get(pid);
@@ -194,6 +260,19 @@ final class BatteryController {
        }
    }

    // Query the battery state for the device and notify all listeners if there is a change.
    private void handleBatteryChangeNotification(int deviceId, long eventTime) {
        synchronized (mLock) {
            final MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId);
            if (deviceState == null) {
                return;
            }
            if (deviceState.updateBatteryState(eventTime)) {
                notifyAllListenersForDeviceLocked(deviceState);
            }
        }
    }

    void dump(PrintWriter pw, String prefix) {
        synchronized (mLock) {
            pw.println(prefix + TAG + ": " + mListenerRecords.size()
@@ -211,6 +290,29 @@ final class BatteryController {
        }
    }

    @VisibleForTesting
    @Override
    public void onInputDeviceAdded(int deviceId) {}

    @VisibleForTesting
    @Override
    public void onInputDeviceRemoved(int deviceId) {}

    @VisibleForTesting
    @Override
    public void onInputDeviceChanged(int deviceId) {
        synchronized (mLock) {
            final MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId);
            if (deviceState == null) {
                return;
            }
            final long eventTime = SystemClock.uptimeMillis();
            if (deviceState.updateBatteryState(eventTime)) {
                notifyAllListenersForDeviceLocked(deviceState);
            }
        }
    }

    // A record of a registered battery listener from one process.
    private class ListenerRecord {
        final int mPid;
@@ -232,4 +334,109 @@ final class BatteryController {
                    + ", monitored devices=" + Arrays.toString(mMonitoredDevices.toArray());
        }
    }

    // Holds the state of an InputDevice for which battery changes are currently being monitored.
    private class MonitoredDeviceState {
        private final int mDeviceId;

        private long mLastUpdateTime = 0;
        private boolean mHasBattery = false;
        @BatteryState.BatteryStatus
        private int mBatteryStatus = BatteryState.STATUS_UNKNOWN;
        private float mBatteryCapacity = Float.NaN;

        @Nullable
        private UEventListener mUEventListener;

        MonitoredDeviceState(int deviceId) {
            mDeviceId = deviceId;

            // Load the initial battery state and start monitoring.
            final long eventTime = SystemClock.uptimeMillis();
            updateBatteryState(eventTime);
        }

        // Returns true if the battery state changed since the last time it was updated.
        boolean updateBatteryState(long eventTime) {
            mLastUpdateTime = eventTime;

            final boolean batteryPresenceChanged = mHasBattery != hasBattery(mDeviceId);
            if (batteryPresenceChanged) {
                mHasBattery = !mHasBattery;
                if (mHasBattery) {
                    startMonitoring();
                } else {
                    stopMonitoring();
                }
            }

            final int oldStatus = mBatteryStatus;
            final float oldCapacity = mBatteryCapacity;

            if (mHasBattery) {
                mBatteryStatus = mNative.getBatteryStatus(mDeviceId);
                mBatteryCapacity = mNative.getBatteryCapacity(mDeviceId) / 100.f;
            } else {
                mBatteryStatus = BatteryState.STATUS_UNKNOWN;
                mBatteryCapacity = Float.NaN;
            }

            return batteryPresenceChanged
                    || mBatteryStatus != oldStatus
                    || mBatteryCapacity != oldCapacity;
        }

        private void startMonitoring() {
            final String batteryPath = mNative.getBatteryDevicePath(mDeviceId);
            if (batteryPath == null) {
                return;
            }
            mUEventListener = new UEventListener() {
                @Override
                void onUEvent(long eventTime) {
                    handleBatteryChangeNotification(mDeviceId, eventTime);
                }
            };
            mUEventManager.addListener(mUEventListener, "DEVPATH=" + batteryPath);
        }

        // This must be called when the device is no longer being monitored.
        void stopMonitoring() {
            if (mUEventListener != null) {
                mUEventManager.removeListener(mUEventListener);
                mUEventListener = null;
            }
        }
    }

    // An interface used to change the API of UEventObserver to a more test-friendly format.
    @VisibleForTesting
    interface UEventManager {

        @VisibleForTesting
        abstract class UEventListener {
            private final UEventObserver mObserver = new UEventObserver() {
                @Override
                public void onUEvent(UEvent event) {
                    final long eventTime = SystemClock.uptimeMillis();
                    if (DEBUG) {
                        Slog.d(TAG,
                                "UEventListener: Received UEvent: "
                                        + event + " eventTime: " + eventTime);
                    }
                    UEventListener.this.onUEvent(eventTime);
                }
            };

            abstract void onUEvent(long eventTime);
        }

        default void addListener(UEventListener listener, String match) {
            listener.mObserver.startObserving(match);
        }

        default void removeListener(UEventListener listener) {
            listener.mObserver.stopObserving();
        }
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -421,7 +421,7 @@ public class InputManagerService extends IInputManager.Stub
        mContext = injector.getContext();
        mHandler = new InputManagerHandler(injector.getLooper());
        mNative = injector.getNativeService(this);
        mBatteryController = new BatteryController(mContext, mNative);
        mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());

        mUseDevInputEventForAudioJack =
                mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
@@ -561,6 +561,8 @@ public class InputManagerService extends IInputManager.Stub
        if (mWiredAccessoryCallbacks != null) {
            mWiredAccessoryCallbacks.systemReady();
        }

        mBatteryController.systemRunning();
    }

    private void reloadKeyboardLayouts() {
+98 −10
Original line number Diff line number Diff line
@@ -20,16 +20,20 @@ import android.content.Context
import android.content.ContextWrapper
import android.hardware.BatteryState.STATUS_CHARGING
import android.hardware.BatteryState.STATUS_FULL
import android.hardware.BatteryState.STATUS_UNKNOWN
import android.hardware.input.IInputDeviceBatteryListener
import android.hardware.input.IInputDevicesChangedListener
import android.hardware.input.IInputManager
import android.hardware.input.InputDeviceCountryCode
import android.hardware.input.InputManager
import android.os.Binder
import android.os.IBinder
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
import androidx.test.InstrumentationRegistry
import com.android.server.input.BatteryController.UEventManager
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
@@ -39,15 +43,27 @@ import org.mockito.ArgumentMatchers.notNull
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.eq
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.`when`
import org.mockito.junit.MockitoJUnit

private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice =
    InputDevice.Builder()
        .setId(deviceId)
        .setName("Device $deviceId")
        .setDescriptor("descriptor $deviceId")
        .setExternal(true)
        .setHasBattery(hasBattery)
        .setGeneration(0)
        .build()

/**
 * Tests for {@link InputDeviceBatteryController}.
 *
@@ -60,6 +76,7 @@ class BatteryControllerTests {
        const val PID = 42
        const val DEVICE_ID = 13
        const val SECOND_DEVICE_ID = 11
        const val TIMESTAMP = 123456789L
    }

    @get:Rule
@@ -69,13 +86,19 @@ class BatteryControllerTests {
    private lateinit var native: NativeInputManagerService
    @Mock
    private lateinit var iInputManager: IInputManager
    @Mock
    private lateinit var uEventManager: UEventManager

    private lateinit var batteryController: BatteryController
    private lateinit var context: Context
    private lateinit var testLooper: TestLooper
    private lateinit var devicesChangedListener: IInputDevicesChangedListener
    private val deviceGenerationMap = mutableMapOf<Int /*deviceId*/, Int /*generation*/>()

    @Before
    fun setup() {
        context = spy(ContextWrapper(InstrumentationRegistry.getContext()))
        testLooper = TestLooper()
        val inputManager = InputManager.resetInstance(iInputManager)
        `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
        `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID))
@@ -83,17 +106,18 @@ class BatteryControllerTests {
        `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
            .thenReturn(createInputDevice(SECOND_DEVICE_ID))

        batteryController = BatteryController(context, native)
        batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
        batteryController.systemRunning()
        val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java)
        verify(iInputManager).registerInputDevicesChangedListener(listenerCaptor.capture())
        devicesChangedListener = listenerCaptor.value
    }

    private fun createInputDevice(deviceId: Int): InputDevice =
        InputDevice.Builder()
            .setId(deviceId)
            .setName("Device $deviceId")
            .setDescriptor("descriptor $deviceId")
            .setExternal(true)
            .setHasBattery(true)
            .build()
    private fun notifyDeviceChanged(deviceId: Int) {
        deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1
        val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) }
        devicesChangedListener.onInputDevicesChanged(list.toIntArray())
    }

    @After
    fun tearDown() {
@@ -169,4 +193,68 @@ class BatteryControllerTests {
        verify(listener).onBatteryStateChanged(eq(SECOND_DEVICE_ID), eq(true /*isPresent*/),
            eq(STATUS_CHARGING), eq(0.78f), anyLong())
    }

    @Test
    fun testListenersNotifiedOnUEventNotification() {
        `when`(native.getBatteryDevicePath(DEVICE_ID)).thenReturn("/test/device1")
        `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING)
        `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78)
        val listener = createMockListener()
        val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java)
        batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
        verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
        verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
            eq(STATUS_CHARGING), eq(0.78f), anyLong())

        // If the battery state has changed when an UEvent is sent, the listeners are notified.
        `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
        uEventListener.value!!.onUEvent(TIMESTAMP)
        verify(listener).onBatteryStateChanged(DEVICE_ID, true /*isPresent*/, STATUS_CHARGING,
            0.80f, TIMESTAMP)

        // If the battery state has not changed when an UEvent is sent, the listeners are not
        // notified.
        clearInvocations(listener)
        uEventListener.value!!.onUEvent(TIMESTAMP + 1)
        verifyNoMoreInteractions(listener)

        batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID)
        verify(uEventManager).removeListener(uEventListener.capture())
        assertEquals("The same observer must be registered and unregistered",
            uEventListener.allValues[0], uEventListener.allValues[1])
    }

    @Test
    fun testBatteryPresenceChanged() {
        `when`(native.getBatteryDevicePath(DEVICE_ID)).thenReturn("/test/device1")
        `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING)
        `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78)
        val listener = createMockListener()
        val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java)
        batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
        verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
        verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
            eq(STATUS_CHARGING), eq(0.78f), anyLong())

        // If the battery presence for the InputDevice changes, the listener is notified.
        `when`(iInputManager.getInputDevice(DEVICE_ID))
            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false))
        notifyDeviceChanged(DEVICE_ID)
        testLooper.dispatchNext()
        verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(false /*isPresent*/),
            eq(STATUS_UNKNOWN), eq(Float.NaN), anyLong())
        // Since the battery is no longer present, the UEventListener should be removed.
        verify(uEventManager).removeListener(uEventListener.value)

        // If the battery becomes present again, the listener is notified.
        `when`(iInputManager.getInputDevice(DEVICE_ID))
            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true))
        notifyDeviceChanged(DEVICE_ID)
        testLooper.dispatchNext()
        verify(listener, times(2)).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
            eq(STATUS_CHARGING), eq(0.78f), anyLong())
        // Ensure that a new UEventListener was added.
        verify(uEventManager, times(2))
            .addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
    }
}