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

Commit 3e1f8b55 authored by Michał Brzeziński's avatar Michał Brzeziński Committed by Automerger Merge Worker
Browse files

Merge "Adding newlyConnectedKeyboard flow to KeyboardRepository" into udc-dev am: 889b6b4c

parents 04b2f159 889b6b4c
Loading
Loading
Loading
Loading
+3 −1
Original line number Original line 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.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)
        }
        }
}
}
+20 −0
Original line number Original line 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 Original line 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.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
+4 −4
Original line number Original line Diff line number Diff line
@@ -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()
    }
    }
+2 −2
Original line number Original line Diff line number Diff line
@@ -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))
Loading