diff options
3 files changed, 203 insertions, 4 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 05246870e2fe..7864f1901e57 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -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(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index d9ede44faa2e..3e111e6de785 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -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() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt index e6ec20e0ea73..58b55602a39c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -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 |