Loading packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt 0 → 100644 +136 −0 Original line number Diff line number Diff line /* * 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() } } packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt 0 → 100644 +289 −0 Original line number Diff line number Diff line /* * 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()) } } Loading
packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt 0 → 100644 +136 −0 Original line number Diff line number Diff line /* * 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() } }
packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt 0 → 100644 +289 −0 Original line number Diff line number Diff line /* * 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()) } }