Merge "Adding newlyConnectedKeyboard flow to KeyboardRepository" into udc-dev

This commit is contained in:
Michał Brzeziński
2023-05-10 15:04:03 +00:00
committed by Android (Google) Code Review
7 changed files with 204 additions and 36 deletions

View File

@@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.keyboard.data.repository.KeyboardRepository
import com.android.systemui.keyboard.shared.model.BacklightModel import com.android.systemui.keyboard.shared.model.BacklightModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@@ -34,8 +35,9 @@ constructor(
) { ) {
/** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */ /** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */
@ExperimentalCoroutinesApi
val backlight: Flow<BacklightModel?> = val backlight: Flow<BacklightModel?> =
keyboardRepository.keyboardConnected.flatMapLatest { connected -> keyboardRepository.isAnyKeyboardConnected.flatMapLatest { connected ->
if (connected) keyboardRepository.backlight else flowOf(null) if (connected) keyboardRepository.backlight else flowOf(null)
} }
} }

View File

@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 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.keyboard.data.model
data class Keyboard(val vendorId: Int, val productId: Int)

View File

@@ -25,22 +25,41 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel import com.android.systemui.keyboard.shared.model.BacklightModel
import java.util.concurrent.Executor import java.util.concurrent.Executor
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
interface KeyboardRepository { interface KeyboardRepository {
val keyboardConnected: Flow<Boolean> /** Emits true if any physical keyboard is connected to the device, false otherwise. */
val isAnyKeyboardConnected: Flow<Boolean>
/**
* Emits [Keyboard] object whenever new physical keyboard connects. When SysUI (re)starts it
* emits all currently connected keyboards
*/
val newlyConnectedKeyboard: Flow<Keyboard>
/**
* Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only
* happen when physical keyboard is connected
*/
val backlight: Flow<BacklightModel> val backlight: Flow<BacklightModel>
} }
@@ -53,33 +72,65 @@ constructor(
private val inputManager: InputManager, private val inputManager: InputManager,
) : KeyboardRepository { ) : KeyboardRepository {
private val connectedDeviceIds: Flow<Set<Int>> = private sealed interface DeviceChange
private data class DeviceAdded(val deviceId: Int) : DeviceChange
private object DeviceRemoved : DeviceChange
private object FreshStart : DeviceChange
/**
* Emits collection of all currently connected keyboards and what was the last [DeviceChange].
* It emits collection so that every new subscriber to this SharedFlow can get latest state of
* all keyboards. Otherwise we might get into situation where subscriber timing on
* initialization matter and later subscriber will only get latest device and will miss all
* previous devices.
*/
private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> =
conflatedCallbackFlow { conflatedCallbackFlow {
var connectedKeyboards = inputManager.inputDeviceIds.toSet() var connectedDevices = inputManager.inputDeviceIds.toSet()
val listener = val listener =
object : InputManager.InputDeviceListener { object : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) { override fun onInputDeviceAdded(deviceId: Int) {
connectedKeyboards = connectedKeyboards + deviceId connectedDevices = connectedDevices + deviceId
sendWithLogging(connectedKeyboards) sendWithLogging(connectedDevices to DeviceAdded(deviceId))
} }
override fun onInputDeviceChanged(deviceId: Int) = Unit override fun onInputDeviceChanged(deviceId: Int) = Unit
override fun onInputDeviceRemoved(deviceId: Int) { override fun onInputDeviceRemoved(deviceId: Int) {
connectedKeyboards = connectedKeyboards - deviceId connectedDevices = connectedDevices - deviceId
sendWithLogging(connectedKeyboards) sendWithLogging(connectedDevices to DeviceRemoved)
} }
} }
sendWithLogging(connectedKeyboards) sendWithLogging(connectedDevices to FreshStart)
inputManager.registerInputDeviceListener(listener, /* handler= */ null) inputManager.registerInputDeviceListener(listener, /* handler= */ null)
awaitClose { inputManager.unregisterInputDeviceListener(listener) } awaitClose { inputManager.unregisterInputDeviceListener(listener) }
} }
.map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
.shareIn( .shareIn(
scope = applicationScope, scope = applicationScope,
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
replay = 1, replay = 1,
) )
@FlowPreview
override val newlyConnectedKeyboard: Flow<Keyboard> =
keyboardsChange
.flatMapConcat { (devices, operation) ->
when (operation) {
FreshStart -> devices.asFlow()
is DeviceAdded -> flowOf(operation.deviceId)
is DeviceRemoved -> emptyFlow()
}
}
.mapNotNull { deviceIdToKeyboard(it) }
.flowOn(backgroundDispatcher)
override val isAnyKeyboardConnected: Flow<Boolean> =
keyboardsChange
.map { (devices, _) -> devices.isNotEmpty() }
.distinctUntilChanged()
.flowOn(backgroundDispatcher)
private val backlightStateListener: Flow<KeyboardBacklightState> = conflatedCallbackFlow { private val backlightStateListener: Flow<KeyboardBacklightState> = conflatedCallbackFlow {
val listener = KeyboardBacklightListener { _, state, isTriggeredByKeyPress -> val listener = KeyboardBacklightListener { _, state, isTriggeredByKeyPress ->
if (isTriggeredByKeyPress) { if (isTriggeredByKeyPress) {
@@ -90,11 +141,10 @@ constructor(
awaitClose { inputManager.unregisterKeyboardBacklightListener(listener) } awaitClose { inputManager.unregisterKeyboardBacklightListener(listener) }
} }
override val keyboardConnected: Flow<Boolean> = private fun deviceIdToKeyboard(deviceId: Int): Keyboard? {
connectedDeviceIds val device = inputManager.getInputDevice(deviceId) ?: return null
.map { it.any { deviceId -> isPhysicalFullKeyboard(deviceId) } } return Keyboard(device.vendorId, device.productId)
.distinctUntilChanged() }
.flowOn(backgroundDispatcher)
override val backlight: Flow<BacklightModel> = override val backlight: Flow<BacklightModel> =
backlightStateListener backlightStateListener

View File

@@ -47,14 +47,14 @@ class KeyboardBacklightInteractorTest : SysuiTestCase() {
@Test @Test
fun emitsNull_whenKeyboardJustConnected() = runTest { fun emitsNull_whenKeyboardJustConnected() = runTest {
val latest by collectLastValue(underTest.backlight) val latest by collectLastValue(underTest.backlight)
keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true)
assertThat(latest).isNull() assertThat(latest).isNull()
} }
@Test @Test
fun emitsBacklight_whenKeyboardConnectedAndBacklightChanged() = runTest { fun emitsBacklight_whenKeyboardConnectedAndBacklightChanged() = runTest {
keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(1, 5)) keyboardRepository.setBacklight(BacklightModel(1, 5))
assertThat(underTest.backlight.first()).isEqualTo(BacklightModel(1, 5)) assertThat(underTest.backlight.first()).isEqualTo(BacklightModel(1, 5))
@@ -63,10 +63,10 @@ class KeyboardBacklightInteractorTest : SysuiTestCase() {
@Test @Test
fun emitsNull_afterKeyboardDisconnecting() = runTest { fun emitsNull_afterKeyboardDisconnecting() = runTest {
val latest by collectLastValue(underTest.backlight) val latest by collectLastValue(underTest.backlight)
keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(1, 5)) keyboardRepository.setBacklight(BacklightModel(1, 5))
keyboardRepository.setKeyboardConnected(false) keyboardRepository.setIsAnyKeyboardConnected(false)
assertThat(latest).isNull() assertThat(latest).isNull()
} }

View File

@@ -58,7 +58,7 @@ class BacklightDialogViewModelTest : SysuiTestCase() {
KeyboardBacklightInteractor(keyboardRepository), KeyboardBacklightInteractor(keyboardRepository),
accessibilityManagerWrapper accessibilityManagerWrapper
) )
keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true)
} }
@Test @Test
@@ -81,7 +81,7 @@ class BacklightDialogViewModelTest : SysuiTestCase() {
@Test @Test
fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest { fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest {
val latest by collectLastValue(underTest.dialogContent) val latest by collectLastValue(underTest.dialogContent)
keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(1, 5)) keyboardRepository.setBacklight(BacklightModel(1, 5))
assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5)) assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5))

View File

@@ -25,6 +25,8 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.nullable
@@ -36,6 +38,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
@@ -78,7 +81,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
@Test @Test
fun emitsDisconnected_ifNothingIsConnected() = fun emitsDisconnected_ifNothingIsConnected() =
testScope.runTest { testScope.runTest {
val initialState = underTest.keyboardConnected.first() val initialState = underTest.isAnyKeyboardConnected.first()
assertThat(initialState).isFalse() assertThat(initialState).isFalse()
} }
@@ -86,7 +89,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() = fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() =
testScope.runTest { testScope.runTest {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID)) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
val initialValue = underTest.keyboardConnected.first() val initialValue = underTest.isAnyKeyboardConnected.first()
assertThat(initialValue).isTrue() assertThat(initialValue).isTrue()
} }
@@ -94,7 +97,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_whenNewPhysicalKeyboardConnects() = fun emitsConnected_whenNewPhysicalKeyboardConnects() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
@@ -105,7 +108,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenDeviceWithIdDoesNotExist() = fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(NULL_DEVICE_ID) deviceListener.onInputDeviceAdded(NULL_DEVICE_ID)
assertThat(isKeyboardConnected).isFalse() assertThat(isKeyboardConnected).isFalse()
@@ -115,7 +118,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenKeyboardDisconnects() = fun emitsDisconnected_whenKeyboardDisconnects() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isTrue() assertThat(isKeyboardConnected).isTrue()
@@ -125,7 +128,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
} }
private suspend fun captureDeviceListener(): InputManager.InputDeviceListener { private suspend fun captureDeviceListener(): InputManager.InputDeviceListener {
underTest.keyboardConnected.first() underTest.isAnyKeyboardConnected.first()
verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable()) verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
return deviceListenerCaptor.value return deviceListenerCaptor.value
} }
@@ -134,7 +137,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() = fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID) deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isFalse() assertThat(isKeyboardConnected).isFalse()
@@ -147,7 +150,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() = fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isFalse() assertThat(isKeyboardConnected).isFalse()
@@ -157,7 +160,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_whenAnotherDeviceDisconnects() = fun emitsConnected_whenAnotherDeviceDisconnects() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID)
@@ -169,7 +172,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() = fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() =
testScope.runTest { testScope.runTest {
val deviceListener = captureDeviceListener() val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected) val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID) deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
@@ -205,7 +208,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
@Test @Test
fun keyboardBacklightValuesNotPassed_fromBacklightListener_whenNotTriggeredByKeyPress() { fun keyboardBacklightValuesNotPassed_fromBacklightListener_whenNotTriggeredByKeyPress() {
testScope.runTest() { testScope.runTest {
val backlight by collectLastValueImmediately(underTest.backlight) val backlight by collectLastValueImmediately(underTest.backlight)
verify(inputManager) verify(inputManager)
.registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture()) .registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
@@ -221,7 +224,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
@Test @Test
fun passesKeyboardBacklightValues_fromBacklightListener_whenTriggeredByKeyPress() { fun passesKeyboardBacklightValues_fromBacklightListener_whenTriggeredByKeyPress() {
testScope.runTest() { testScope.runTest {
val backlight by collectLastValueImmediately(underTest.backlight) val backlight by collectLastValueImmediately(underTest.backlight)
verify(inputManager) verify(inputManager)
.registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture()) .registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
@@ -235,6 +238,86 @@ class KeyboardRepositoryTest : SysuiTestCase() {
} }
} }
@Test
fun passessAllKeyboards_thatWereAlreadyConnectedOnInitialization() {
testScope.runTest {
whenever(inputManager.inputDeviceIds)
.thenReturn(
intArrayOf(
PHYSICAL_FULL_KEYBOARD_ID,
ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
VIRTUAL_FULL_KEYBOARD_ID // not a physical keyboard - that's why result is 2
)
)
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
assertThat(keyboards).hasSize(2)
}
}
@Test
fun passesNewlyConnectedKeyboard() {
testScope.runTest {
val deviceListener = captureDeviceListener()
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(underTest.newlyConnectedKeyboard.first())
.isEqualTo(Keyboard(VENDOR_ID, PRODUCT_ID))
}
}
@Test
fun emitsOnlyNewlyConnectedKeyboards() {
testScope.runTest {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
underTest.newlyConnectedKeyboard.first()
verify(inputManager)
.registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
val deviceListener = deviceListenerCaptor.value
deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
assertThat(keyboards).hasSize(1)
}
}
@Test
fun stillEmitsNewKeyboardEvenIfFlowWasSubscribedAfterOtherFlows() {
testScope.runTest {
whenever(inputManager.inputDeviceIds)
.thenReturn(
intArrayOf(
PHYSICAL_FULL_KEYBOARD_ID,
ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
VIRTUAL_FULL_KEYBOARD_ID // not a physical keyboard - that's why result is 2
)
)
collectLastValueImmediately(underTest.isAnyKeyboardConnected)
// let's pretend second flow is subscribed after some delay
advanceTimeBy(1000)
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
assertThat(keyboards).hasSize(2)
}
}
@Test
fun emitsKeyboardWhenItWasReconnected() {
testScope.runTest {
val deviceListener = captureDeviceListener()
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(keyboards).hasSize(2)
}
}
private fun KeyboardBacklightListener.onBacklightChanged( private fun KeyboardBacklightListener.onBacklightChanged(
current: Int, current: Int,
max: Int, max: Int,
@@ -254,6 +337,9 @@ class KeyboardRepositoryTest : SysuiTestCase() {
private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4 private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4
private const val NULL_DEVICE_ID = 5 private const val NULL_DEVICE_ID = 5
private const val VENDOR_ID = 99
private const val PRODUCT_ID = 101
private val INPUT_DEVICES_MAP: Map<Int, InputDevice> = private val INPUT_DEVICES_MAP: Map<Int, InputDevice> =
mapOf( mapOf(
PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true), PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true),
@@ -267,6 +353,8 @@ class KeyboardRepositoryTest : SysuiTestCase() {
mock<InputDevice>().also { mock<InputDevice>().also {
whenever(it.isVirtual).thenReturn(virtual) whenever(it.isVirtual).thenReturn(virtual)
whenever(it.isFullKeyboard).thenReturn(fullKeyboard) whenever(it.isFullKeyboard).thenReturn(fullKeyboard)
whenever(it.vendorId).thenReturn(VENDOR_ID)
whenever(it.productId).thenReturn(PRODUCT_ID)
} }
} }

View File

@@ -17,6 +17,7 @@
package com.android.systemui.keyboard.data.repository package com.android.systemui.keyboard.data.repository
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel import com.android.systemui.keyboard.shared.model.BacklightModel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,18 +25,25 @@ import kotlinx.coroutines.flow.filterNotNull
class FakeKeyboardRepository : KeyboardRepository { class FakeKeyboardRepository : KeyboardRepository {
private val _keyboardConnected = MutableStateFlow(false) private val _isAnyKeyboardConnected = MutableStateFlow(false)
override val keyboardConnected: Flow<Boolean> = _keyboardConnected override val isAnyKeyboardConnected: Flow<Boolean> = _isAnyKeyboardConnected
private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null) private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null)
// filtering to make sure backlight doesn't have default initial value // filtering to make sure backlight doesn't have default initial value
override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull() override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
fun setBacklight(state: BacklightModel) { fun setBacklight(state: BacklightModel) {
_backlightState.value = state _backlightState.value = state
} }
fun setKeyboardConnected(connected: Boolean) { fun setIsAnyKeyboardConnected(connected: Boolean) {
_keyboardConnected.value = connected _isAnyKeyboardConnected.value = connected
}
fun setNewlyConnectedKeyboard(keyboard: Keyboard) {
_newlyConnectedKeyboard.value = keyboard
} }
} }