Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 50db6657 authored by Michal Brzezinski's avatar Michal Brzezinski
Browse files

Adding newlyConnectedKeyboard flow to KeyboardRepository

newlyConnectedKeyboard emits new physical Keyboards that are connected to the device.

Other changes:
1. Because keyboardConnected starts to be confusing now, I renamed it to isAnyKeyboardConnected.
2. Extracting  inputDeviceListener flow to separate shared flow so that both newlyConnectedKeyboard and isAnyKeyboardConnected can use it without each registering their own listener.
3. Commenting KeyboardRepository flows
4. Adding tests for newlyConnectedKeyboard

Flag: KEYBOARD_EDUCATION
Bug: 277192620
Test: KeyboardRepositoryTest
Change-Id: I61044362a4e924d80546ae92300844280174db00
parent f6adbe0d
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -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)
        }
}
+20 −0
Original line number Diff line number Diff line
/*
 * 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)
+63 −13
Original line number Diff line number Diff line
@@ -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
+4 −4
Original line number Diff line number Diff line
@@ -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()
    }
+2 −2
Original line number Diff line number Diff line
@@ -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))
Loading