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

Commit 2b2b0da5 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Add InputDeviceBatteryListener to InputManager" into tm-qpr-dev am: 666111df

parents c6165102 666111df
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 IInputDeviceBatteryListener {

    /**
     * Called when there is a change in battery state for a monitored device. This will be called
     * immediately after the listener is successfully registered for a new device via IInputManager.
     * The parameters are values exposed through {@link android.hardware.BatteryState}.
     */
    void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, float capacity,
            long eventTime);
}
+5 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.graphics.Rect;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.KeyboardLayout;
import android.hardware.input.IInputDevicesChangedListener;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.ITabletModeChangedListener;
import android.hardware.input.TouchCalibration;
import android.os.CombinedVibration;
@@ -155,4 +156,8 @@ interface IInputManager {
    void closeLightSession(int deviceId, in IBinder token);

    void cancelCurrentTouch();

    void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener);

    void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener);
}
+207 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.app.ActivityThread;
import android.compat.annotation.ChangeId;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.SensorManager;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
@@ -66,6 +67,7 @@ import android.view.PointerIcon;
import android.view.VerifiedInputEvent;
import android.view.WindowManager.LayoutParams;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
@@ -74,6 +76,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * Provides information about input devices and available key layouts.
@@ -111,6 +115,14 @@ public final class InputManager {
    private TabletModeChangedListener mTabletModeChangedListener;
    private List<OnTabletModeChangedListenerDelegate> mOnTabletModeChangedListeners;

    private final Object mBatteryListenersLock = new Object();
    // Maps a deviceId whose battery is currently being monitored to an entry containing the
    // registered listeners for that device.
    @GuardedBy("mBatteryListenersLock")
    private SparseArray<RegisteredBatteryListeners> mBatteryListeners;
    @GuardedBy("mBatteryListenersLock")
    private IInputDeviceBatteryListener mInputDeviceBatteryListener;

    private InputDeviceSensorManager mInputDeviceSensorManager;
    /**
     * Broadcast Action: Query available keyboard layouts.
@@ -1751,6 +1763,129 @@ public final class InputManager {
        }
    }

    /**
     * Adds a battery listener to be notified about {@link BatteryState} changes for an input
     * device. The same listener can be registered for multiple input devices.
     * The listener will be notified of the initial battery state of the device after it is
     * successfully registered.
     * @param deviceId the input device that should be monitored
     * @param executor an executor on which the callback will be called
     * @param listener the {@link InputDeviceBatteryListener}
     * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener)
     * @hide
     */
    public void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor,
            @NonNull InputDeviceBatteryListener listener) {
        Objects.requireNonNull(executor, "executor should not be null");
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mBatteryListenersLock) {
            if (mBatteryListeners == null) {
                mBatteryListeners = new SparseArray<>();
                mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener();
            }
            RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
            if (listenersForDevice == null) {
                // The deviceId is currently not being monitored for battery changes.
                // Start monitoring the device.
                listenersForDevice = new RegisteredBatteryListeners();
                mBatteryListeners.put(deviceId, listenersForDevice);
                try {
                    mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            } else {
                // The deviceId is already being monitored for battery changes.
                // Ensure that the listener is not already registered.
                for (InputDeviceBatteryListenerDelegate delegate : listenersForDevice.mDelegates) {
                    if (Objects.equals(listener, delegate.mListener)) {
                        throw new IllegalArgumentException(
                                "Attempting to register an InputDeviceBatteryListener that has "
                                        + "already been registered for deviceId: "
                                        + deviceId);
                    }
                }
            }
            final InputDeviceBatteryListenerDelegate delegate =
                    new InputDeviceBatteryListenerDelegate(listener, executor);
            listenersForDevice.mDelegates.add(delegate);

            // Notify the listener immediately if we already have the latest battery state.
            if (listenersForDevice.mLatestBatteryState != null) {
                delegate.notifyBatteryStateChanged(listenersForDevice.mLatestBatteryState);
            }
        }
    }

    /**
     * Removes a previously registered battery listener for an input device.
     * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener)
     * @hide
     */
    public void removeInputDeviceBatteryListener(int deviceId,
            @NonNull InputDeviceBatteryListener listener) {
        Objects.requireNonNull(listener, "listener should not be null");

        synchronized (mBatteryListenersLock) {
            if (mBatteryListeners == null) {
                return;
            }
            RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId);
            if (listenersForDevice == null) {
                // The deviceId is not currently being monitored.
                return;
            }
            final List<InputDeviceBatteryListenerDelegate> delegates =
                    listenersForDevice.mDelegates;
            for (int i = 0; i < delegates.size();) {
                if (Objects.equals(listener, delegates.get(i).mListener)) {
                    delegates.remove(i);
                    continue;
                }
                i++;
            }
            if (!delegates.isEmpty()) {
                return;
            }

            // There are no more battery listeners for this deviceId. Stop monitoring this device.
            mBatteryListeners.remove(deviceId);
            try {
                mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
            if (mBatteryListeners.size() == 0) {
                // There are no more devices being monitored, so the registered
                // IInputDeviceBatteryListener will be automatically dropped by the server.
                mBatteryListeners = null;
                mInputDeviceBatteryListener = null;
            }
        }
    }

    /**
     * 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.
     * @see InputDevice#getBatteryState()
     * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener)
     * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener)
     * @hide
     */
    public interface InputDeviceBatteryListener {
        /**
         * Called when the battery state of an input device changes.
         * @param deviceId the input device for which the battery changed.
         * @param eventTimeMillis the time (in ms) when the battery change took place.
         *        This timestamp is in the {@link SystemClock#uptimeMillis()} time base.
         * @param batteryState the new battery state, never null.
         */
        void onBatteryStateChanged(
                int deviceId, long eventTimeMillis, @NonNull BatteryState batteryState);
    }

    /**
     * Listens for changes in input devices.
     */
@@ -1861,4 +1996,76 @@ public final class InputManager {
            }
        }
    }

    private static final class LocalBatteryState extends BatteryState {
        final int mDeviceId;
        final boolean mIsPresent;
        final int mStatus;
        final float mCapacity;
        final long mEventTime;

        LocalBatteryState(int deviceId, boolean isPresent, int status, float capacity,
                long eventTime) {
            mDeviceId = deviceId;
            mIsPresent = isPresent;
            mStatus = status;
            mCapacity = capacity;
            mEventTime = eventTime;
        }

        @Override
        public boolean isPresent() {
            return mIsPresent;
        }

        @Override
        public int getStatus() {
            return mStatus;
        }

        @Override
        public float getCapacity() {
            return mCapacity;
        }
    }

    private static final class RegisteredBatteryListeners {
        final List<InputDeviceBatteryListenerDelegate> mDelegates = new ArrayList<>();
        LocalBatteryState mLatestBatteryState;
    }

    private static final class InputDeviceBatteryListenerDelegate {
        final InputDeviceBatteryListener mListener;
        final Executor mExecutor;

        InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) {
            mListener = listener;
            mExecutor = executor;
        }

        void notifyBatteryStateChanged(LocalBatteryState batteryState) {
            mExecutor.execute(() ->
                    mListener.onBatteryStateChanged(batteryState.mDeviceId, batteryState.mEventTime,
                            batteryState));
        }
    }

    private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub {
        @Override
        public void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status,
                float capacity, long eventTime) {
            synchronized (mBatteryListenersLock) {
                if (mBatteryListeners == null) return;
                final RegisteredBatteryListeners entry = mBatteryListeners.get(deviceId);
                if (entry == null) return;

                entry.mLatestBatteryState =
                        new LocalBatteryState(
                                deviceId, isBatteryPresent, status, capacity, eventTime);
                for (InputDeviceBatteryListenerDelegate delegate : entry.mDelegates) {
                    delegate.notifyBatteryStateChanged(entry.mLatestBatteryState);
                }
            }
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -1021,6 +1021,15 @@ public final class InputDevice implements Parcelable {
        InputManager.getInstance().setCustomPointerIcon(icon);
    }

    /**
     * Reports whether the device has a battery.
     * @return true if the device has a battery, false otherwise.
     * @hide
     */
    public boolean hasBattery() {
        return mHasBattery;
    }

    /**
     * Provides information about the range of values for a particular {@link MotionEvent} axis.
     *
+227 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.hardware.BatteryState
import android.os.Handler
import android.os.HandlerExecutor
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import com.android.server.testutils.any
import java.util.concurrent.Executor
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
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.anyInt
import org.mockito.Mockito.doAnswer
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoJUnitRunner

/**
 * Tests for [InputManager.InputDeviceBatteryListener].
 *
 * Build/Install/Run:
 * atest FrameworksCoreTests:InputDeviceBatteryListenerTest
 */
@Presubmit
@RunWith(MockitoJUnitRunner::class)
class InputDeviceBatteryListenerTest {
    @get:Rule
    val rule = MockitoJUnit.rule()!!

    private lateinit var testLooper: TestLooper
    private var registeredListener: IInputDeviceBatteryListener? = null
    private val monitoredDevices = mutableListOf<Int>()
    private lateinit var executor: Executor
    private lateinit var inputManager: InputManager

    @Mock
    private lateinit var iInputManagerMock: IInputManager

    @Before
    fun setUp() {
        testLooper = TestLooper()
        executor = HandlerExecutor(Handler(testLooper.looper))
        registeredListener = null
        monitoredDevices.clear()
        inputManager = InputManager.resetInstance(iInputManagerMock)

        // Handle battery listener registration.
        doAnswer {
            val deviceId = it.getArgument(0) as Int
            val listener = it.getArgument(1) as IInputDeviceBatteryListener
            if (registeredListener != null &&
                    registeredListener!!.asBinder() != listener.asBinder()) {
                // There can only be one registered battery listener per process.
                fail("Trying to register a new listener when one already exists")
            }
            if (monitoredDevices.contains(deviceId)) {
                fail("Trying to start monitoring a device that was already being monitored")
            }
            monitoredDevices.add(deviceId)
            registeredListener = listener
            null
        }.`when`(iInputManagerMock).registerBatteryListener(anyInt(), any())

        // Handle battery listener being unregistered.
        doAnswer {
            val deviceId = it.getArgument(0) as Int
            val listener = it.getArgument(1) as IInputDeviceBatteryListener
            if (registeredListener == null ||
                    registeredListener!!.asBinder() != listener.asBinder()) {
                fail("Trying to unregister a listener that is not registered")
            }
            if (!monitoredDevices.remove(deviceId)) {
                fail("Trying to stop monitoring a device that is not being monitored")
            }
            if (monitoredDevices.isEmpty()) {
                registeredListener = null
            }
        }.`when`(iInputManagerMock).unregisterBatteryListener(anyInt(), any())
    }

    @After
    fun tearDown() {
        InputManager.clearInstance()
    }

    private fun notifyBatteryStateChanged(
        deviceId: Int,
        isPresent: Boolean = true,
        status: Int = BatteryState.STATUS_FULL,
        capacity: Float = 1.0f,
        eventTime: Long = 12345L
    ) {
        registeredListener!!.onBatteryStateChanged(deviceId, isPresent, status, capacity, eventTime)
    }

    @Test
    fun testListenerIsNotifiedCorrectly() {
        var callbackCount = 0

        // Add a battery listener to monitor battery changes.
        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor) {
                deviceId: Int, eventTime: Long, batteryState: BatteryState ->
            callbackCount++
            assertEquals(1, deviceId)
            assertEquals(true, batteryState.isPresent)
            assertEquals(BatteryState.STATUS_DISCHARGING, batteryState.status)
            assertEquals(0.5f, batteryState.capacity)
            assertEquals(8675309L, eventTime)
        }

        // Adding the listener should register the callback with InputManagerService.
        assertNotNull(registeredListener)
        assertTrue(monitoredDevices.contains(1))

        // Notifying battery change for a different device should not trigger the listener.
        notifyBatteryStateChanged(deviceId = 2)
        testLooper.dispatchAll()
        assertEquals(0, callbackCount)

        // Notifying battery change for the registered device will notify the listener.
        notifyBatteryStateChanged(1 /*deviceId*/, true /*isPresent*/,
            BatteryState.STATUS_DISCHARGING, 0.5f /*capacity*/, 8675309L /*eventTime*/)
        testLooper.dispatchNext()
        assertEquals(1, callbackCount)
    }

    @Test
    fun testMultipleListeners() {
        // Set up two callbacks.
        var callbackCount1 = 0
        var callbackCount2 = 0
        val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ }
        val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ }

        // Monitor battery changes for three devices. The first callback monitors devices 1 and 3,
        // while the second callback monitors devices 2 and 3.
        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1)
        assertEquals(1, monitoredDevices.size)
        inputManager.addInputDeviceBatteryListener(2 /*deviceId*/, executor, callback2)
        assertEquals(2, monitoredDevices.size)
        inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback1)
        assertEquals(3, monitoredDevices.size)
        inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback2)
        assertEquals(3, monitoredDevices.size)

        // Notifying battery change for each of the devices should trigger the registered callbacks.
        notifyBatteryStateChanged(deviceId = 1)
        testLooper.dispatchNext()
        assertEquals(1, callbackCount1)
        assertEquals(0, callbackCount2)

        notifyBatteryStateChanged(deviceId = 2)
        testLooper.dispatchNext()
        assertEquals(1, callbackCount1)
        assertEquals(1, callbackCount2)

        notifyBatteryStateChanged(deviceId = 3)
        testLooper.dispatchNext()
        testLooper.dispatchNext()
        assertEquals(2, callbackCount1)
        assertEquals(2, callbackCount2)

        // Stop monitoring devices 1 and 2.
        inputManager.removeInputDeviceBatteryListener(1 /*deviceId*/, callback1)
        assertEquals(2, monitoredDevices.size)
        inputManager.removeInputDeviceBatteryListener(2 /*deviceId*/, callback2)
        assertEquals(1, monitoredDevices.size)

        // Ensure device 3 continues to be monitored.
        notifyBatteryStateChanged(deviceId = 3)
        testLooper.dispatchNext()
        testLooper.dispatchNext()
        assertEquals(3, callbackCount1)
        assertEquals(3, callbackCount2)

        // Stop monitoring all devices.
        inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback1)
        assertEquals(1, monitoredDevices.size)
        inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback2)
        assertEquals(0, monitoredDevices.size)
    }

    @Test
    fun testAdditionalListenersNotifiedImmediately() {
        var callbackCount1 = 0
        var callbackCount2 = 0
        val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ }
        val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ }

        // Add a battery listener and send the latest battery state.
        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1)
        assertEquals(1, monitoredDevices.size)
        notifyBatteryStateChanged(deviceId = 1)
        testLooper.dispatchNext()
        assertEquals(1, callbackCount1)

        // Add a second listener for the same device that already has the latest battery state.
        inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback2)
        assertEquals(1, monitoredDevices.size)

        // Ensure that this listener is notified immediately.
        testLooper.dispatchNext()
        assertEquals(1, callbackCount2)
    }
}
Loading