diff options
6 files changed, 301 insertions, 535 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt deleted file mode 100644 index 154c6e2e3158..000000000000 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt +++ /dev/null @@ -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() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 302d6a9ca1b7..235495cfa50d 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 11233dda165c..14a9161ac291 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -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 { diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt deleted file mode 100644 index 8dd088f5760c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt +++ /dev/null @@ -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()) - } -} 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 984de5b67bf5..6d6e40a90fa6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -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,19 +89,47 @@ 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) @@ -117,6 +142,26 @@ class StylusManagerTest : SysuiTestCase() { } @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) @@ -198,6 +275,17 @@ class StylusManagerTest : SysuiTestCase() { } @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) stylusManager.registerCallback(otherStylusCallback) @@ -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() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt index ff382a3ec19f..117e00dd9b2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -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() { - 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) - } + fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID)) - @Test - fun onStylusAdded_externalStylus_doesNotAddBatteryListener() { - startable.onStylusAdded(EXTERNAL_DEVICE_ID) - - verify(inputManager, never()) - .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + startable.start() - @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 } |