Move stylus first usage detection into StylusManager.

Instead of being its own separate CoreStartable, first stylus use
detection is now part of StylusManager. This means features needing this
information are now responsible for starting and listening to the StylusManager.

Battery listeners are also no longer unregistered upon first stylus use,
as now, it triggers the StylusBatteryCallback, which will be needed for
USI low battery detection.

Bug: 251206662
Test: atest StylusManagerTest
Change-Id: I120fd0e35ee92483bdce90a27d5f1d38ef60b4d5
This commit is contained in:
Vania Januar
2022-12-20 13:25:10 +00:00
parent 83844dfc2a
commit f3dc2166ba
6 changed files with 301 additions and 535 deletions

View File

@@ -1,136 +0,0 @@
/*
* 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 com.android.systemui.stylus
import android.content.Context
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.util.Log
import android.view.InputDevice
import androidx.annotation.VisibleForTesting
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import java.util.concurrent.Executor
import javax.inject.Inject
/**
* A listener that detects when a stylus has first been used, by detecting 1) the presence of an
* internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth
* address.
*/
@SysUISingleton
class StylusFirstUsageListener
@Inject
constructor(
private val context: Context,
private val inputManager: InputManager,
private val stylusManager: StylusManager,
private val featureFlags: FeatureFlags,
@Background private val executor: Executor,
@Background private val handler: Handler,
) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
// Set must be only accessed from the background handler, which is the same handler that
// runs the StylusManager callbacks.
private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf()
@VisibleForTesting var hasStarted = false
override fun start() {
if (true) return // TODO(b/261826950): remove on main
if (hasStarted) return
if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
if (inputManager.isStylusEverUsed(context)) return
if (!hostDeviceSupportsStylusInput()) return
hasStarted = true
inputManager.inputDeviceIds.forEach(this::onStylusAdded)
stylusManager.registerCallback(this)
stylusManager.startListener()
}
override fun onStylusAdded(deviceId: Int) {
if (!hasStarted) return
val device = inputManager.getInputDevice(deviceId) ?: return
if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return
try {
inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
internalStylusDeviceIds += deviceId
} catch (e: SecurityException) {
Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.")
}
}
override fun onStylusRemoved(deviceId: Int) {
if (!hasStarted) return
if (!internalStylusDeviceIds.contains(deviceId)) return
try {
inputManager.removeInputDeviceBatteryListener(deviceId, this)
internalStylusDeviceIds.remove(deviceId)
} catch (e: SecurityException) {
Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
}
}
override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
if (!hasStarted) return
onRemoteDeviceFound()
}
override fun onBatteryStateChanged(
deviceId: Int,
eventTimeMillis: Long,
batteryState: BatteryState
) {
if (!hasStarted) return
if (batteryState.isPresent) {
onRemoteDeviceFound()
}
}
private fun onRemoteDeviceFound() {
inputManager.setStylusEverUsed(context, true)
cleanupListeners()
}
private fun cleanupListeners() {
stylusManager.unregisterCallback(this)
handler.post {
internalStylusDeviceIds.forEach {
inputManager.removeInputDeviceBatteryListener(it, this)
}
}
}
private fun hostDeviceSupportsStylusInput(): Boolean {
return inputManager.inputDeviceIds
.asSequence()
.mapNotNull { inputManager.getInputDevice(it) }
.any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
}
companion object {
private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty()
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.stylus
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.util.ArrayMap
@@ -25,6 +27,8 @@ import android.util.Log
import android.view.InputDevice
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -37,25 +41,37 @@ import javax.inject.Inject
class StylusManager
@Inject
constructor(
private val context: Context,
private val inputManager: InputManager,
private val bluetoothAdapter: BluetoothAdapter?,
@Background private val handler: Handler,
@Background private val executor: Executor,
) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {
private val featureFlags: FeatureFlags,
) :
InputManager.InputDeviceListener,
InputManager.InputDeviceBatteryListener,
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()
// This variable should only be accessed on the handler
private var hasStarted: Boolean = false
/**
* Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
* at time of starting.
*/
fun startListener() {
addExistingStylusToMap()
inputManager.registerInputDeviceListener(this, handler)
handler.post {
if (hasStarted) return@post
hasStarted = true
addExistingStylusToMap()
inputManager.registerInputDeviceListener(this, handler)
}
}
/** Registers a StylusCallback to listen to stylus events. */
@@ -77,21 +93,30 @@ constructor(
}
override fun onInputDeviceAdded(deviceId: Int) {
if (!hasStarted) return
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
if (!device.isExternal) {
registerBatteryListener(deviceId)
}
// TODO(b/257936830): get address once input api available
val btAddress: String? = null
inputDeviceAddressMap[deviceId] = btAddress
executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
if (btAddress != null) {
onStylusUsed()
onStylusBluetoothConnected(btAddress)
executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
}
}
override fun onInputDeviceChanged(deviceId: Int) {
if (!hasStarted) return
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
@@ -112,7 +137,10 @@ constructor(
}
override fun onInputDeviceRemoved(deviceId: Int) {
if (!hasStarted) return
if (!inputDeviceAddressMap.contains(deviceId)) return
unregisterBatteryListener(deviceId)
val btAddress: String? = inputDeviceAddressMap[deviceId]
inputDeviceAddressMap.remove(deviceId)
@@ -124,13 +152,14 @@ constructor(
}
override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
handler.post executeMetadataChanged@{
if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null)
return@executeMetadataChanged
handler.post {
if (!hasStarted) return@post
if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post
val inputDeviceId: Int =
inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
?: return@executeMetadataChanged
?: return@post
val isCharging = String(value) == "true"
@@ -140,6 +169,24 @@ constructor(
}
}
override fun onBatteryStateChanged(
deviceId: Int,
eventTimeMillis: Long,
batteryState: BatteryState
) {
handler.post {
if (!hasStarted) return@post
if (batteryState.isPresent) {
onStylusUsed()
}
executeStylusBatteryCallbacks { cb ->
cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState)
}
}
}
private fun onStylusBluetoothConnected(btAddress: String) {
val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
try {
@@ -158,6 +205,21 @@ constructor(
}
}
/**
* An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a
* physical stylus device has never been used. This method is run when 1) a USI stylus battery
* event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a
* physical stylus device has actually been used.
*/
private fun onStylusUsed() {
if (true) return // TODO(b/261826950): remove on main
if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
if (inputManager.isStylusEverUsed(context)) return
inputManager.setStylusEverUsed(context, true)
executeStylusCallbacks { cb -> cb.onStylusFirstUsed() }
}
private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
stylusCallbacks.forEach(run)
}
@@ -166,31 +228,69 @@ constructor(
stylusBatteryCallbacks.forEach(run)
}
private fun registerBatteryListener(deviceId: Int) {
try {
inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
} catch (e: SecurityException) {
Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
}
}
private fun unregisterBatteryListener(deviceId: Int) {
// If deviceId wasn't registered, the result is a no-op, so an "is registered"
// check is not needed.
try {
inputManager.removeInputDeviceBatteryListener(deviceId, this)
} catch (e: SecurityException) {
Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
}
}
private fun addExistingStylusToMap() {
for (deviceId: Int in inputManager.inputDeviceIds) {
val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
// TODO(b/257936830): get address once input api available
inputDeviceAddressMap[deviceId] = null
if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
// For most devices, an active (non-bluetooth) stylus is represented by an
// internal InputDevice. This InputDevice will be present in InputManager
// before CoreStartables run, and will not be removed.
// In many cases, it reports the battery level of the stylus.
registerBatteryListener(deviceId)
}
}
}
}
/** Callback interface to receive events from the StylusManager. */
/**
* Callback interface to receive events from the StylusManager. All callbacks are run on the
* same background handler.
*/
interface StylusCallback {
fun onStylusAdded(deviceId: Int) {}
fun onStylusRemoved(deviceId: Int) {}
fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {}
fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
fun onStylusFirstUsed() {}
}
/** Callback interface to receive stylus battery events from the StylusManager. */
/**
* Callback interface to receive stylus battery events from the StylusManager. All callbacks are
* runs on the same background handler.
*/
interface StylusBatteryCallback {
fun onStylusBluetoothChargingStateChanged(
inputDeviceId: Int,
btDevice: BluetoothDevice,
isCharging: Boolean
) {}
fun onStylusUsiBatteryStateChanged(
deviceId: Int,
eventTimeMillis: Long,
batteryState: BatteryState,
) {}
}
companion object {

View File

@@ -18,14 +18,11 @@ package com.android.systemui.stylus
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.util.Log
import android.view.InputDevice
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import java.util.concurrent.Executor
import javax.inject.Inject
/**
@@ -40,16 +37,7 @@ constructor(
private val inputManager: InputManager,
private val stylusUsiPowerUi: StylusUsiPowerUI,
private val featureFlags: FeatureFlags,
@Background private val executor: Executor,
) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener {
override fun onStylusAdded(deviceId: Int) {
val device = inputManager.getInputDevice(deviceId) ?: return
if (!device.isExternal) {
registerBatteryListener(deviceId)
}
}
) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback {
override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
stylusUsiPowerUi.refresh()
@@ -59,15 +47,7 @@ constructor(
stylusUsiPowerUi.refresh()
}
override fun onStylusRemoved(deviceId: Int) {
val device = inputManager.getInputDevice(deviceId) ?: return
if (!device.isExternal) {
unregisterBatteryListener(deviceId)
}
}
override fun onBatteryStateChanged(
override fun onStylusUsiBatteryStateChanged(
deviceId: Int,
eventTimeMillis: Long,
batteryState: BatteryState
@@ -77,39 +57,19 @@ constructor(
}
}
private fun registerBatteryListener(deviceId: Int) {
try {
inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
} catch (e: SecurityException) {
Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
}
}
private fun unregisterBatteryListener(deviceId: Int) {
try {
inputManager.removeInputDeviceBatteryListener(deviceId, this)
} catch (e: SecurityException) {
Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.")
}
}
override fun start() {
if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return
addBatteryListenerForInternalStyluses()
if (!hostDeviceSupportsStylusInput()) return
stylusManager.registerCallback(this)
stylusManager.startListener()
}
private fun addBatteryListenerForInternalStyluses() {
// For most devices, an active stylus is represented by an internal InputDevice.
// This InputDevice will be present in InputManager before CoreStartables run,
// and will not be removed. In many cases, it reports the battery level of the stylus.
inputManager.inputDeviceIds
private fun hostDeviceSupportsStylusInput(): Boolean {
return inputManager.inputDeviceIds
.asSequence()
.mapNotNull { inputManager.getInputDevice(it) }
.filter { it.supportsSource(InputDevice.SOURCE_STYLUS) }
.forEach { onStylusAdded(it.id) }
.any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal }
}
companion object {

View File

@@ -1,289 +0,0 @@
/*
* 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 com.android.systemui.stylus
import android.content.Context
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.view.InputDevice
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
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.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
@Ignore("TODO(b/20579491): unignore on main")
class StylusFirstUsageListenerTest : SysuiTestCase() {
@Mock lateinit var context: Context
@Mock lateinit var inputManager: InputManager
@Mock lateinit var stylusManager: StylusManager
@Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var internalStylusDevice: InputDevice
@Mock lateinit var otherDevice: InputDevice
@Mock lateinit var externalStylusDevice: InputDevice
@Mock lateinit var batteryState: BatteryState
@Mock lateinit var handler: Handler
private lateinit var stylusListener: StylusFirstUsageListener
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
whenever(inputManager.isStylusEverUsed(context)).thenReturn(false)
stylusListener =
StylusFirstUsageListener(
context,
inputManager,
stylusManager,
featureFlags,
EXECUTOR,
handler
)
stylusListener.hasStarted = false
whenever(handler.post(any())).thenAnswer {
(it.arguments[0] as Runnable).run()
true
}
whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
whenever(internalStylusDevice.isExternal).thenReturn(false)
whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
whenever(externalStylusDevice.isExternal).thenReturn(true)
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID))
.thenReturn(internalStylusDevice)
whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID))
.thenReturn(externalStylusDevice)
}
@Test
fun start_flagDisabled_doesNotRegister() {
whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false)
stylusListener.start()
verify(stylusManager, never()).registerCallback(any())
verify(inputManager, never()).setStylusEverUsed(context, true)
}
@Test
fun start_toggleHasStarted() {
stylusListener.start()
assert(stylusListener.hasStarted)
}
@Test
fun start_hasStarted_doesNotRegister() {
stylusListener.hasStarted = true
stylusListener.start()
verify(stylusManager, never()).registerCallback(any())
}
@Test
fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID))
stylusListener.start()
verify(stylusManager, never()).registerCallback(any())
verify(inputManager, never()).setStylusEverUsed(context, true)
}
@Test
fun start_stylusEverUsed_doesNotRegister() {
whenever(inputManager.inputDeviceIds)
.thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
whenever(inputManager.isStylusEverUsed(context)).thenReturn(true)
stylusListener.start()
verify(stylusManager, never()).registerCallback(any())
verify(inputManager, never()).setStylusEverUsed(context, true)
}
@Test
fun start_hostDeviceSupportsStylus_registersListener() {
whenever(inputManager.inputDeviceIds)
.thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID))
stylusListener.start()
verify(stylusManager).registerCallback(any())
verify(inputManager, never()).setStylusEverUsed(context, true)
}
@Test
fun onStylusAdded_hasNotStarted_doesNotRegisterListener() {
stylusListener.hasStarted = false
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
verifyZeroInteractions(inputManager)
}
@Test
fun onStylusAdded_internalStylus_registersListener() {
stylusListener.hasStarted = true
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
verify(inputManager, times(1))
.addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener)
}
@Test
fun onStylusAdded_externalStylus_doesNotRegisterListener() {
stylusListener.hasStarted = true
stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID)
verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
}
@Test
fun onStylusAdded_otherDevice_doesNotRegisterListener() {
stylusListener.onStylusAdded(OTHER_DEVICE_ID)
verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any())
}
@Test
fun onStylusRemoved_registeredDevice_unregistersListener() {
stylusListener.hasStarted = true
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
verify(inputManager, times(1))
.removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
}
@Test
fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() {
stylusListener.hasStarted = false
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
verifyZeroInteractions(inputManager)
}
@Test
fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() {
stylusListener.hasStarted = true
stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID)
verifyNoMoreInteractions(inputManager)
}
@Test
fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() {
stylusListener.hasStarted = true
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
verify(inputManager).setStylusEverUsed(context, true)
verify(inputManager, times(1))
.removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
verify(stylusManager).unregisterCallback(stylusListener)
}
@Test
fun onStylusBluetoothConnected_hasNotStarted_doesNoting() {
stylusListener.hasStarted = false
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY")
verifyZeroInteractions(inputManager)
verifyZeroInteractions(stylusManager)
}
@Test
fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() {
stylusListener.hasStarted = true
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
whenever(batteryState.isPresent).thenReturn(true)
stylusListener.onBatteryStateChanged(0, 1, batteryState)
verify(inputManager).setStylusEverUsed(context, true)
verify(inputManager, times(1))
.removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
verify(stylusManager).unregisterCallback(stylusListener)
}
@Test
fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() {
stylusListener.hasStarted = true
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
whenever(batteryState.isPresent).thenReturn(false)
stylusListener.onBatteryStateChanged(0, 1, batteryState)
verifyZeroInteractions(stylusManager)
verify(inputManager, never())
.removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener)
}
@Test
fun onBatteryStateChanged_hasNotStarted_doesNothing() {
stylusListener.hasStarted = false
stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID)
whenever(batteryState.isPresent).thenReturn(false)
stylusListener.onBatteryStateChanged(0, 1, batteryState)
verifyZeroInteractions(inputManager)
verifyZeroInteractions(stylusManager)
}
companion object {
private const val OTHER_DEVICE_ID = 0
private const val INTERNAL_STYLUS_DEVICE_ID = 1
private const val EXTERNAL_STYLUS_DEVICE_ID = 2
private val EXECUTOR = FakeExecutor(FakeSystemClock())
}
}

View File

@@ -17,12 +17,15 @@ package com.android.systemui.stylus
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.hardware.BatteryState
import android.hardware.input.InputManager
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.view.InputDevice
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.Executor
@@ -31,30 +34,27 @@ import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
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.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
@Ignore("b/257936830 until bt APIs")
class StylusManagerTest : SysuiTestCase() {
@Mock lateinit var inputManager: InputManager
@Mock lateinit var stylusDevice: InputDevice
@Mock lateinit var btStylusDevice: InputDevice
@Mock lateinit var otherDevice: InputDevice
@Mock lateinit var batteryState: BatteryState
@Mock lateinit var bluetoothAdapter: BluetoothAdapter
@Mock lateinit var bluetoothDevice: BluetoothDevice
@Mock lateinit var handler: Handler
@Mock lateinit var featureFlags: FeatureFlags
@Mock lateinit var stylusCallback: StylusManager.StylusCallback
@@ -75,11 +75,8 @@ class StylusManagerTest : SysuiTestCase() {
true
}
stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR)
stylusManager.registerCallback(stylusCallback)
stylusManager.registerBatteryCallback(stylusBatteryCallback)
stylusManager =
StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
@@ -92,18 +89,46 @@ 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(inputManager.isStylusEverUsed(mContext)).thenReturn(false)
whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice)
whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS)
whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true)
stylusManager.startListener()
stylusManager.registerCallback(stylusCallback)
stylusManager.registerBatteryCallback(stylusBatteryCallback)
clearInvocations(inputManager)
}
@Test
fun startListener_registersInputDeviceListener() {
fun startListener_hasNotStarted_registersInputDeviceListener() {
stylusManager =
StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
stylusManager.startListener()
verify(inputManager, times(1)).registerInputDeviceListener(any(), any())
}
@Test
fun startListener_hasStarted_doesNothing() {
stylusManager.startListener()
verifyZeroInteractions(inputManager)
}
@Test
fun onInputDeviceAdded_hasNotStarted_doesNothing() {
stylusManager =
StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
verifyZeroInteractions(stylusCallback)
}
@Test
fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() {
stylusManager.registerCallback(otherStylusCallback)
@@ -116,6 +141,26 @@ class StylusManagerTest : SysuiTestCase() {
verifyNoMoreInteractions(otherStylusCallback)
}
@Test
fun onInputDeviceAdded_internalStylus_registersBatteryListener() {
whenever(stylusDevice.isExternal).thenReturn(false)
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
verify(inputManager, times(1))
.addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager)
}
@Test
fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() {
whenever(stylusDevice.isExternal).thenReturn(true)
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
verify(inputManager, never())
.addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager)
}
@Test
fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
@@ -125,6 +170,23 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(stylusCallback, times(1)).onStylusFirstUsed()
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(inputManager, times(1)).setStylusEverUsed(mContext, true)
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -143,6 +205,17 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
fun onInputDeviceChanged_hasNotStarted_doesNothing() {
stylusManager =
StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
verifyZeroInteractions(stylusCallback)
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
// whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
@@ -157,6 +230,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
// whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
@@ -168,6 +242,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
// whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
@@ -179,6 +254,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -189,6 +265,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
@@ -197,6 +274,17 @@ class StylusManagerTest : SysuiTestCase() {
verify(stylusCallback, never()).onStylusBluetoothDisconnected(any(), any())
}
@Test
fun onInputDeviceRemoved_hasNotStarted_doesNothing() {
stylusManager =
StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags)
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
verifyZeroInteractions(stylusCallback)
}
@Test
fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
@@ -219,6 +307,17 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
fun onInputDeviceRemoved_unregistersBatteryListener() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
verify(inputManager, times(1))
.removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onInputDeviceRemoved_btStylus_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -232,6 +331,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothConnected_registersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -239,6 +339,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)
@@ -248,6 +349,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -257,6 +359,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.registerBatteryCallback(otherStylusBatteryCallback)
@@ -274,6 +377,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -288,6 +392,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -302,6 +407,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
stylusManager.onMetadataChanged(
bluetoothDevice,
@@ -313,6 +419,7 @@ class StylusManagerTest : SysuiTestCase() {
}
@Test
@Ignore("b/257936830 until bt APIs")
fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
@@ -326,6 +433,63 @@ class StylusManagerTest : SysuiTestCase() {
.onStylusBluetoothChargingStateChanged(any(), any(), any())
}
@Test
@Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(inputManager).setStylusEverUsed(mContext, true)
}
@Test
@Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(stylusCallback, times(1)).onStylusFirstUsed()
}
@Test
@Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() {
whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true)
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(inputManager, never()).setStylusEverUsed(mContext, true)
}
@Test
@Ignore("TODO(b/261826950): remove on main")
fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() {
whenever(batteryState.isPresent).thenReturn(false)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(inputManager, never())
.removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
}
@Test
fun onBatteryStateChanged_hasNotStarted_doesNothing() {
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verifyZeroInteractions(inputManager)
}
@Test
fun onBatteryStateChanged_executesBatteryCallbacks() {
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(stylusBatteryCallback, times(1))
.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
}
companion object {
private val EXECUTOR = Executor { r -> r.run() }

View File

@@ -25,17 +25,15 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@@ -60,7 +58,6 @@ class StylusUsiPowerStartableTest : SysuiTestCase() {
inputManager,
stylusUsiPowerUi,
featureFlags,
DIRECT_EXECUTOR,
)
whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true)
@@ -79,40 +76,12 @@ class StylusUsiPowerStartableTest : SysuiTestCase() {
}
@Test
fun start_addsBatteryListenerForInternalStylus() {
fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID))
startable.start()
verify(inputManager, times(1))
.addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
}
@Test
fun onStylusAdded_internalStylus_addsBatteryListener() {
startable.onStylusAdded(STYLUS_DEVICE_ID)
verify(inputManager, times(1))
.addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
}
@Test
fun onStylusAdded_externalStylus_doesNotAddBatteryListener() {
startable.onStylusAdded(EXTERNAL_DEVICE_ID)
verify(inputManager, never())
.addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable)
}
@Test
fun onStylusRemoved_registeredStylus_removesBatteryListener() {
startable.onStylusAdded(STYLUS_DEVICE_ID)
startable.onStylusRemoved(STYLUS_DEVICE_ID)
inOrder(inputManager).let {
it.verify(inputManager, times(1))
.addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable)
it.verify(inputManager, times(1))
.removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable)
}
verifyZeroInteractions(stylusManager)
}
@Test
@@ -130,28 +99,26 @@ class StylusUsiPowerStartableTest : SysuiTestCase() {
}
@Test
fun onBatteryStateChanged_batteryPresent_refreshesNotification() {
fun onStylusUsiBatteryStateChanged_batteryPresent_refreshesNotification() {
val batteryState = mock(BatteryState::class.java)
whenever(batteryState.isPresent).thenReturn(true)
startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState)
}
@Test
fun onBatteryStateChanged_batteryNotPresent_noop() {
fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() {
val batteryState = mock(BatteryState::class.java)
whenever(batteryState.isPresent).thenReturn(false)
startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState)
verifyNoMoreInteractions(stylusUsiPowerUi)
}
companion object {
private val DIRECT_EXECUTOR = Executor { r -> r.run() }
private const val EXTERNAL_DEVICE_ID = 0
private const val STYLUS_DEVICE_ID = 1
}