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

Commit b5e7f591 authored by Vania Januar's avatar Vania Januar
Browse files

Listen to charging state metadata in StylusManager

StylusManager now allows listening to charging state changes by
registering a battery callback (StylusBatteryCallback).

Test: StylusManagerTest
Change-Id: I40dadeb331ff87ac9f5af1ada588b228f7d8d3b2
parent a2b56610
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ import android.app.job.JobScheduler;
import android.app.role.RoleManager;
import android.app.smartspace.SmartspaceManager;
import android.app.trust.TrustManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -609,4 +611,16 @@ public class FrameworkServicesModule {
    static CameraManager provideCameraManager(Context context) {
        return context.getSystemService(CameraManager.class);
    }

    @Provides
    @Singleton
    static BluetoothManager provideBluetoothManager(Context context) {
        return context.getSystemService(BluetoothManager.class);
    }

    @Provides
    @Singleton
    static BluetoothAdapter provideBluetoothAdapter(BluetoothManager bluetoothManager) {
        return bluetoothManager.getAdapter();
    }
}
+69 −1
Original line number Diff line number Diff line
@@ -16,13 +16,17 @@

package com.android.systemui.stylus

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.hardware.input.InputManager
import android.os.Handler
import android.util.ArrayMap
import android.util.Log
import android.view.InputDevice
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import javax.inject.Inject

/**
@@ -34,10 +38,14 @@ class StylusManager
@Inject
constructor(
    private val inputManager: InputManager,
    private val bluetoothAdapter: BluetoothAdapter,
    @Background private val handler: Handler,
) : InputManager.InputDeviceListener {
    @Background private val executor: Executor,
) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {

    private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
    private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> =
        CopyOnWriteArrayList()
    // This map should only be accessed on the handler
    private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()

@@ -60,6 +68,14 @@ constructor(
        stylusCallbacks.remove(callback)
    }

    fun registerBatteryCallback(callback: StylusBatteryCallback) {
        stylusBatteryCallbacks.add(callback)
    }

    fun unregisterBatteryCallback(callback: StylusBatteryCallback) {
        stylusBatteryCallbacks.remove(callback)
    }

    override fun onInputDeviceAdded(deviceId: Int) {
        val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
        if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
@@ -70,6 +86,7 @@ constructor(
        executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }

        if (btAddress != null) {
            onStylusBluetoothConnected(btAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
        }
    }
@@ -84,10 +101,12 @@ constructor(
        inputDeviceAddressMap[deviceId] = currAddress

        if (prevAddress == null && currAddress != null) {
            onStylusBluetoothConnected(currAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, currAddress) }
        }

        if (prevAddress != null && currAddress == null) {
            onStylusBluetoothDisconnected(prevAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, prevAddress) }
        }
    }
@@ -98,15 +117,55 @@ constructor(
        val btAddress: String? = inputDeviceAddressMap[deviceId]
        inputDeviceAddressMap.remove(deviceId)
        if (btAddress != null) {
            onStylusBluetoothDisconnected(btAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, btAddress) }
        }
        executeStylusCallbacks { cb -> cb.onStylusRemoved(deviceId) }
    }

    override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
        handler.post executeMetadataChanged@{
            if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null)
                return@executeMetadataChanged

            val inputDeviceId: Int =
                inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
                    ?: return@executeMetadataChanged

            val isCharging = String(value) == "true"

            executeStylusBatteryCallbacks { cb ->
                cb.onStylusBluetoothChargingStateChanged(inputDeviceId, device, isCharging)
            }
        }
    }

    private fun onStylusBluetoothConnected(btAddress: String) {
        val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return
        try {
            bluetoothAdapter.addOnMetadataChangedListener(device, executor, this)
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "$e: Metadata listener already registered for device. Ignoring.")
        }
    }

    private fun onStylusBluetoothDisconnected(btAddress: String) {
        val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return
        try {
            bluetoothAdapter.removeOnMetadataChangedListener(device, this)
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "$e: Metadata listener does not exist for device. Ignoring.")
        }
    }

    private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
        stylusCallbacks.forEach(run)
    }

    private fun executeStylusBatteryCallbacks(run: (cb: StylusBatteryCallback) -> Unit) {
        stylusBatteryCallbacks.forEach(run)
    }

    private fun addExistingStylusToMap() {
        for (deviceId: Int in inputManager.inputDeviceIds) {
            val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
@@ -125,6 +184,15 @@ constructor(
        fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
    }

    /** Callback interface to receive stylus battery events from the StylusManager. */
    interface StylusBatteryCallback {
        fun onStylusBluetoothChargingStateChanged(
            inputDeviceId: Int,
            btDevice: BluetoothDevice,
            isCharging: Boolean
        ) {}
    }

    companion object {
        private val TAG = StylusManager::class.simpleName.orEmpty()
    }
+120 −3
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.systemui.stylus

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.hardware.input.InputManager
import android.os.Handler
import android.testing.AndroidTestingRunner
@@ -23,12 +25,17 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations

@RunWith(AndroidTestingRunner::class)
@@ -43,12 +50,20 @@ class StylusManagerTest : SysuiTestCase() {

    @Mock lateinit var otherDevice: InputDevice

    @Mock lateinit var bluetoothAdapter: BluetoothAdapter

    @Mock lateinit var bluetoothDevice: BluetoothDevice

    @Mock lateinit var handler: Handler

    @Mock lateinit var stylusCallback: StylusManager.StylusCallback

    @Mock lateinit var otherStylusCallback: StylusManager.StylusCallback

    @Mock lateinit var stylusBatteryCallback: StylusManager.StylusBatteryCallback

    @Mock lateinit var otherStylusBatteryCallback: StylusManager.StylusBatteryCallback

    private lateinit var stylusManager: StylusManager

    @Before
@@ -60,10 +75,12 @@ class StylusManagerTest : SysuiTestCase() {
            true
        }

        stylusManager = StylusManager(inputManager, handler)
        stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR)

        stylusManager.registerCallback(stylusCallback)

        stylusManager.registerBatteryCallback(stylusBatteryCallback)

        whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
        whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
        whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
@@ -75,13 +92,16 @@ class StylusManagerTest : SysuiTestCase() {
        whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice)
        whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice)
        whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID))

        whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice)
        whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS)
    }

    @Test
    fun startListener_registersInputDeviceListener() {
        stylusManager.startListener()

        verify(inputManager, times(1)).registerInputDeviceListener(stylusManager, handler)
        verify(inputManager, times(1)).registerInputDeviceListener(any(), any())
    }

    @Test
@@ -211,7 +231,104 @@ class StylusManagerTest : SysuiTestCase() {
        }
    }

    @Test
    fun onStylusBluetoothConnected_registersMetadataListener() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        verify(bluetoothAdapter, times(1)).addOnMetadataChangedListener(any(), any(), any())
    }

    @Test
    fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
        whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)

        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        verify(bluetoothAdapter, never()).addOnMetadataChangedListener(any(), any(), any())
    }

    @Test
    fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        stylusManager.onInputDeviceRemoved(BT_STYLUS_DEVICE_ID)

        verify(bluetoothAdapter, times(1)).removeOnMetadataChangedListener(any(), any())
    }

    @Test
    fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
        stylusManager.registerBatteryCallback(otherStylusBatteryCallback)

        stylusManager.onMetadataChanged(
            bluetoothDevice,
            BluetoothDevice.METADATA_MAIN_CHARGING,
            "true".toByteArray()
        )

        verify(stylusBatteryCallback, times(1))
            .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true)
        verify(otherStylusBatteryCallback, times(1))
            .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true)
    }

    @Test
    fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        stylusManager.onMetadataChanged(
            bluetoothDevice,
            BluetoothDevice.METADATA_MAIN_CHARGING,
            "true".toByteArray()
        )

        verify(stylusBatteryCallback, times(1))
            .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true)
    }

    @Test
    fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        stylusManager.onMetadataChanged(
            bluetoothDevice,
            BluetoothDevice.METADATA_MAIN_CHARGING,
            "false".toByteArray()
        )

        verify(stylusBatteryCallback, times(1))
            .onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, false)
    }

    @Test
    fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
        stylusManager.onMetadataChanged(
            bluetoothDevice,
            BluetoothDevice.METADATA_MAIN_CHARGING,
            "true".toByteArray()
        )

        verifyNoMoreInteractions(stylusBatteryCallback)
    }

    @Test
    fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        stylusManager.onMetadataChanged(
            bluetoothDevice,
            BluetoothDevice.METADATA_DEVICE_TYPE,
            "true".toByteArray()
        )

        verify(stylusBatteryCallback, never())
            .onStylusBluetoothChargingStateChanged(any(), any(), any())
    }

    companion object {
        private val EXECUTOR = Executor { r -> r.run() }

        private const val OTHER_DEVICE_ID = 0
        private const val STYLUS_DEVICE_ID = 1
        private const val BT_STYLUS_DEVICE_ID = 2