blob: 6d7941f6f64f29ad112e192cc0b45e27a593d630 [file] [log] [blame]
/*
* 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.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.internal.logging.UiEventLogger
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
import org.junit.Before
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
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 uiEventLogger: UiEventLogger
@Mock lateinit var stylusCallback: StylusManager.StylusCallback
@Mock lateinit var otherStylusCallback: StylusManager.StylusCallback
@Mock lateinit var stylusBatteryCallback: StylusManager.StylusBatteryCallback
@Mock lateinit var otherStylusBatteryCallback: StylusManager.StylusBatteryCallback
private lateinit var stylusManager: StylusManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(handler.post(any())).thenAnswer {
(it.arguments[0] as Runnable).run()
true
}
stylusManager =
StylusManager(
mContext,
inputManager,
bluetoothAdapter,
handler,
EXECUTOR,
featureFlags,
uiEventLogger
)
whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
whenever(stylusDevice.bluetoothAddress).thenReturn(null)
whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
whenever(stylusDevice.batteryState).thenReturn(batteryState)
whenever(batteryState.capacity).thenReturn(0.5f)
whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice)
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_hasNotStarted_registersInputDeviceListener() {
stylusManager =
StylusManager(
mContext,
inputManager,
bluetoothAdapter,
handler,
EXECUTOR,
featureFlags,
uiEventLogger
)
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,
uiEventLogger
)
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
verifyZeroInteractions(stylusCallback)
}
@Test
fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() {
stylusManager.registerCallback(otherStylusCallback)
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
verify(stylusCallback, times(1)).onStylusAdded(STYLUS_DEVICE_ID)
verifyNoMoreInteractions(stylusCallback)
verify(otherStylusCallback, times(1)).onStylusAdded(STYLUS_DEVICE_ID)
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)
verify(stylusCallback, times(1)).onStylusAdded(STYLUS_DEVICE_ID)
verifyNoMoreInteractions(stylusCallback)
}
@Test
fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(stylusCallback, times(1)).onStylusFirstUsed()
}
@Test
fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(inputManager, times(1)).setStylusEverUsed(mContext, true)
}
@Test
fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
inOrder(stylusCallback).let {
it.verify(stylusCallback, times(1)).onStylusAdded(BT_STYLUS_DEVICE_ID)
it.verify(stylusCallback, times(1))
.onStylusBluetoothConnected(BT_STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
}
}
@Test
fun onInputDeviceAdded_notStylus_doesNotCallCallbacks() {
stylusManager.onInputDeviceAdded(OTHER_DEVICE_ID)
verifyNoMoreInteractions(stylusCallback)
}
@Test
fun onInputDeviceChanged_hasNotStarted_doesNothing() {
stylusManager =
StylusManager(
mContext,
inputManager,
bluetoothAdapter,
handler,
EXECUTOR,
featureFlags,
uiEventLogger
)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
verifyZeroInteractions(stylusCallback)
}
@Test
fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
stylusManager.registerCallback(otherStylusCallback)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
verify(stylusCallback, times(1))
.onStylusBluetoothConnected(STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
verify(otherStylusCallback, times(1))
.onStylusBluetoothConnected(STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
}
@Test
fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
verify(stylusCallback, times(1))
.onStylusBluetoothConnected(STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
}
@Test
fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
whenever(btStylusDevice.bluetoothAddress).thenReturn(null)
stylusManager.onInputDeviceChanged(BT_STYLUS_DEVICE_ID)
verify(stylusCallback, times(1))
.onStylusBluetoothDisconnected(BT_STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
}
@Test
fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onInputDeviceChanged(BT_STYLUS_DEVICE_ID)
verify(stylusCallback, times(1))
.onStylusBluetoothConnected(BT_STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
}
@Test
fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID)
verify(stylusCallback, never()).onStylusBluetoothDisconnected(any(), any())
}
@Test
fun onInputDeviceRemoved_hasNotStarted_doesNothing() {
stylusManager =
StylusManager(
mContext,
inputManager,
bluetoothAdapter,
handler,
EXECUTOR,
featureFlags,
uiEventLogger
)
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)
stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
verify(stylusCallback, times(1)).onStylusRemoved(STYLUS_DEVICE_ID)
verify(otherStylusCallback, times(1)).onStylusRemoved(STYLUS_DEVICE_ID)
}
@Test
fun onInputDeviceRemoved_stylus_callsCallbacks() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
verify(stylusCallback, times(1)).onStylusRemoved(STYLUS_DEVICE_ID)
verify(stylusCallback, never()).onStylusBluetoothDisconnected(any(), any())
}
@Test
fun onInputDeviceRemoved_unregistersBatteryListener() {
stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID)
verify(inputManager, times(1))
.removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager)
}
@Test
fun onInputDeviceRemoved_btStylus_callsCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(BT_STYLUS_DEVICE_ID)
inOrder(stylusCallback).let {
it.verify(stylusCallback, times(1))
.onStylusBluetoothDisconnected(BT_STYLUS_DEVICE_ID, STYLUS_BT_ADDRESS)
it.verify(stylusCallback, times(1)).onStylusRemoved(BT_STYLUS_DEVICE_ID)
}
}
@Test
fun onStylusBluetoothConnected_registersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(bluetoothAdapter, times(1)).addOnMetadataChangedListener(any(), any(), any())
}
@Test
fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() {
whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null)
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(bluetoothAdapter, never()).addOnMetadataChangedListener(any(), any(), any())
}
@Test
fun onStylusBluetoothConnected_logsEvent() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
verify(uiEventLogger, times(1)).log(StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED)
}
@Test
fun onStylusBluetoothDisconnected_unregistersMetadataListener() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(BT_STYLUS_DEVICE_ID)
verify(bluetoothAdapter, times(1)).removeOnMetadataChangedListener(any(), any())
}
@Test
fun onStylusBluetoothDisconnected_logsEvent() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onInputDeviceRemoved(BT_STYLUS_DEVICE_ID)
verify(uiEventLogger, times(1)).log(StylusUiEvent.BLUETOOTH_STYLUS_DISCONNECTED)
}
@Test
fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.registerBatteryCallback(otherStylusBatteryCallback)
stylusManager.onMetadataChanged(
bluetoothDevice,
BluetoothDevice.METADATA_MAIN_CHARGING,
"true".toByteArray()
)
verify(stylusBatteryCallback, times(1))
.onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true)
verify(otherStylusBatteryCallback, times(1))
.onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true)
}
@Test
fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onMetadataChanged(
bluetoothDevice,
BluetoothDevice.METADATA_MAIN_CHARGING,
"true".toByteArray()
)
verify(stylusBatteryCallback, times(1))
.onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, true)
}
@Test
fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onMetadataChanged(
bluetoothDevice,
BluetoothDevice.METADATA_MAIN_CHARGING,
"false".toByteArray()
)
verify(stylusBatteryCallback, times(1))
.onStylusBluetoothChargingStateChanged(BT_STYLUS_DEVICE_ID, bluetoothDevice, false)
}
@Test
fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() {
stylusManager.onMetadataChanged(
bluetoothDevice,
BluetoothDevice.METADATA_MAIN_CHARGING,
"true".toByteArray()
)
verifyNoMoreInteractions(stylusBatteryCallback)
}
@Test
fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() {
stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
stylusManager.onMetadataChanged(
bluetoothDevice,
BluetoothDevice.METADATA_DEVICE_TYPE,
"true".toByteArray()
)
verify(stylusBatteryCallback, never())
.onStylusBluetoothChargingStateChanged(any(), any(), any())
}
@Test
fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(inputManager).setStylusEverUsed(mContext, true)
}
@Test
fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(stylusCallback, times(1)).onStylusFirstUsed()
}
@Test
fun onBatteryStateChanged_batteryPresent_notInUsiSession_logsSessionStart() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(uiEventLogger, times(1))
.log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED)
}
@Test
fun onBatteryStateChanged_batteryPresent_inUsiSession_doesNotLogSessionStart() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
clearInvocations(uiEventLogger)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(uiEventLogger, never()).log(any())
}
@Test
fun onBatteryStateChanged_batteryAbsent_notInUsiSession_doesNotLogSessionEnd() {
whenever(batteryState.isPresent).thenReturn(false)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(uiEventLogger, never()).log(any())
}
@Test
fun onBatteryStateChanged_batteryAbsent_inUsiSession_logSessionEnd() {
whenever(batteryState.isPresent).thenReturn(true)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
whenever(batteryState.isPresent).thenReturn(false)
stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
verify(uiEventLogger, times(1)).log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED)
}
@Test
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
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() }
private const val OTHER_DEVICE_ID = 0
private const val STYLUS_DEVICE_ID = 1
private const val BT_STYLUS_DEVICE_ID = 2
private const val STYLUS_BT_ADDRESS = "SOME:ADDRESS"
}
}