summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt70
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt123
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