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.shared.model.BacklightModel
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -34,8 +35,9 @@ constructor(
) {
/** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */
@ExperimentalCoroutinesApi
val backlight: Flow<BacklightModel?> =
keyboardRepository.keyboardConnected.flatMapLatest { connected ->
keyboardRepository.isAnyKeyboardConnected.flatMapLatest { connected ->
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.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asFlow
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.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn
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>
}
@@ -53,33 +72,65 @@ constructor(
private val inputManager: InputManager,
) : 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 {
var connectedKeyboards = inputManager.inputDeviceIds.toSet()
var connectedDevices = inputManager.inputDeviceIds.toSet()
val listener =
object : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) {
connectedKeyboards = connectedKeyboards + deviceId
sendWithLogging(connectedKeyboards)
connectedDevices = connectedDevices + deviceId
sendWithLogging(connectedDevices to DeviceAdded(deviceId))
}
override fun onInputDeviceChanged(deviceId: Int) = Unit
override fun onInputDeviceRemoved(deviceId: Int) {
connectedKeyboards = connectedKeyboards - deviceId
sendWithLogging(connectedKeyboards)
connectedDevices = connectedDevices - deviceId
sendWithLogging(connectedDevices to DeviceRemoved)
}
}
sendWithLogging(connectedKeyboards)
sendWithLogging(connectedDevices to FreshStart)
inputManager.registerInputDeviceListener(listener, /* handler= */ null)
awaitClose { inputManager.unregisterInputDeviceListener(listener) }
}
.map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
.shareIn(
scope = applicationScope,
started = SharingStarted.Lazily,
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 {
val listener = KeyboardBacklightListener { _, state, isTriggeredByKeyPress ->
if (isTriggeredByKeyPress) {
@@ -90,11 +141,10 @@ constructor(
awaitClose { inputManager.unregisterKeyboardBacklightListener(listener) }
}
override val keyboardConnected: Flow<Boolean> =
connectedDeviceIds
.map { it.any { deviceId -> isPhysicalFullKeyboard(deviceId) } }
.distinctUntilChanged()
.flowOn(backgroundDispatcher)
private fun deviceIdToKeyboard(deviceId: Int): Keyboard? {
val device = inputManager.getInputDevice(deviceId) ?: return null
return Keyboard(device.vendorId, device.productId)
}
override val backlight: Flow<BacklightModel> =
backlightStateListener

View File

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

View File

@@ -58,7 +58,7 @@ class BacklightDialogViewModelTest : SysuiTestCase() {
KeyboardBacklightInteractor(keyboardRepository),
accessibilityManagerWrapper
)
keyboardRepository.setKeyboardConnected(true)
keyboardRepository.setIsAnyKeyboardConnected(true)
}
@Test
@@ -81,7 +81,7 @@ class BacklightDialogViewModelTest : SysuiTestCase() {
@Test
fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest {
val latest by collectLastValue(underTest.dialogContent)
keyboardRepository.setKeyboardConnected(true)
keyboardRepository.setIsAnyKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(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.coroutines.FlowValue
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.mock
import com.android.systemui.util.mockito.nullable
@@ -36,6 +38,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -78,7 +81,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
@Test
fun emitsDisconnected_ifNothingIsConnected() =
testScope.runTest {
val initialState = underTest.keyboardConnected.first()
val initialState = underTest.isAnyKeyboardConnected.first()
assertThat(initialState).isFalse()
}
@@ -86,7 +89,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() =
testScope.runTest {
whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
val initialValue = underTest.keyboardConnected.first()
val initialValue = underTest.isAnyKeyboardConnected.first()
assertThat(initialValue).isTrue()
}
@@ -94,7 +97,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_whenNewPhysicalKeyboardConnects() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
@@ -105,7 +108,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(NULL_DEVICE_ID)
assertThat(isKeyboardConnected).isFalse()
@@ -115,7 +118,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenKeyboardDisconnects() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isTrue()
@@ -125,7 +128,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
}
private suspend fun captureDeviceListener(): InputManager.InputDeviceListener {
underTest.keyboardConnected.first()
underTest.isAnyKeyboardConnected.first()
verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
return deviceListenerCaptor.value
}
@@ -134,7 +137,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isFalse()
@@ -147,7 +150,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isFalse()
@@ -157,7 +160,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_whenAnotherDeviceDisconnects() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID)
@@ -169,7 +172,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() =
testScope.runTest {
val deviceListener = captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
@@ -205,7 +208,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
@Test
fun keyboardBacklightValuesNotPassed_fromBacklightListener_whenNotTriggeredByKeyPress() {
testScope.runTest() {
testScope.runTest {
val backlight by collectLastValueImmediately(underTest.backlight)
verify(inputManager)
.registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
@@ -221,7 +224,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
@Test
fun passesKeyboardBacklightValues_fromBacklightListener_whenTriggeredByKeyPress() {
testScope.runTest() {
testScope.runTest {
val backlight by collectLastValueImmediately(underTest.backlight)
verify(inputManager)
.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(
current: Int,
max: Int,
@@ -254,6 +337,9 @@ class KeyboardRepositoryTest : SysuiTestCase() {
private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4
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> =
mapOf(
PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true),
@@ -267,6 +353,8 @@ class KeyboardRepositoryTest : SysuiTestCase() {
mock<InputDevice>().also {
whenever(it.isVirtual).thenReturn(virtual)
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
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,18 +25,25 @@ import kotlinx.coroutines.flow.filterNotNull
class FakeKeyboardRepository : KeyboardRepository {
private val _keyboardConnected = MutableStateFlow(false)
override val keyboardConnected: Flow<Boolean> = _keyboardConnected
private val _isAnyKeyboardConnected = MutableStateFlow(false)
override val isAnyKeyboardConnected: Flow<Boolean> = _isAnyKeyboardConnected
private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null)
// filtering to make sure backlight doesn't have default initial value
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) {
_backlightState.value = state
}
fun setKeyboardConnected(connected: Boolean) {
_keyboardConnected.value = connected
fun setIsAnyKeyboardConnected(connected: Boolean) {
_isAnyKeyboardConnected.value = connected
}
fun setNewlyConnectedKeyboard(keyboard: Keyboard) {
_newlyConnectedKeyboard.value = keyboard
}
}