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
This commit is contained in:
Vania Januar
2022-11-29 12:33:10 +00:00
parent a2b566109f
commit b5e7f591e1
3 changed files with 203 additions and 4 deletions

View File

@@ -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();
}
}

View File

@@ -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()
}

View File

@@ -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