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

Commit 436eaedb authored by Yalan Yiue's avatar Yalan Yiue Committed by Android (Google) Code Review
Browse files

Merge "Move device connection logic to a separated class" into main

parents 6befd099 26c73373
Loading
Loading
Loading
Loading
+95 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.inputdevice.data.repository

import android.annotation.SuppressLint
import android.hardware.input.InputManager
import android.os.Handler
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn

@SysUISingleton
class InputDeviceRepository
@Inject
constructor(
    @Background private val backgroundHandler: Handler,
    @Background private val backgroundScope: CoroutineScope,
    private val inputManager: InputManager
) {

    sealed interface DeviceChange

    data class DeviceAdded(val deviceId: Int) : DeviceChange

    data object DeviceRemoved : DeviceChange

    data 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.
     */
    // TODO(b/351984587): Replace with StateFlow
    @SuppressLint("SharedFlowCreation")
    val deviceChange: Flow<Pair<Collection<Int>, DeviceChange>> =
        conflatedCallbackFlow {
                var connectedDevices = inputManager.inputDeviceIds.toSet()
                val listener =
                    object : InputManager.InputDeviceListener {
                        override fun onInputDeviceAdded(deviceId: Int) {
                            connectedDevices = connectedDevices + deviceId
                            sendWithLogging(connectedDevices to DeviceAdded(deviceId))
                        }

                        override fun onInputDeviceChanged(deviceId: Int) = Unit

                        override fun onInputDeviceRemoved(deviceId: Int) {
                            connectedDevices = connectedDevices - deviceId
                            sendWithLogging(connectedDevices to DeviceRemoved)
                        }
                    }
                sendWithLogging(connectedDevices to FreshStart)
                inputManager.registerInputDeviceListener(listener, backgroundHandler)
                awaitClose { inputManager.unregisterInputDeviceListener(listener) }
            }
            .shareIn(
                scope = backgroundScope,
                started = SharingStarted.Lazily,
                replay = 1,
            )

    private fun <T> SendChannel<T>.sendWithLogging(element: T) {
        trySendWithFailureLogging(element, TAG)
    }

    companion object {
        const val TAG = "InputDeviceRepository"
    }
}
+10 −44
Original line number Diff line number Diff line
@@ -21,21 +21,23 @@ import android.hardware.input.InputManager
import android.hardware.input.InputManager.KeyboardBacklightListener
import android.hardware.input.KeyboardBacklightState
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceAdded
import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceChange
import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceRemoved
import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.FreshStart
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
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
@@ -44,7 +46,6 @@ 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

/**
 * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be
@@ -71,50 +72,15 @@ interface KeyboardRepository {
class KeyboardRepositoryImpl
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val inputManager: InputManager,
    inputDeviceRepository: InputDeviceRepository
) : KeyboardRepository {

    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 connectedDevices = inputManager.inputDeviceIds.toSet()
                val listener =
                    object : InputManager.InputDeviceListener {
                        override fun onInputDeviceAdded(deviceId: Int) {
                            connectedDevices = connectedDevices + deviceId
                            sendWithLogging(connectedDevices to DeviceAdded(deviceId))
                        }

                        override fun onInputDeviceChanged(deviceId: Int) = Unit

                        override fun onInputDeviceRemoved(deviceId: Int) {
                            connectedDevices = connectedDevices - deviceId
                            sendWithLogging(connectedDevices to DeviceRemoved)
                        }
                    }
                sendWithLogging(connectedDevices to FreshStart)
                inputManager.registerInputDeviceListener(listener, /* handler= */ null)
                awaitClose { inputManager.unregisterInputDeviceListener(listener) }
        inputDeviceRepository.deviceChange.map { (ids, change) ->
            ids.filter { id -> isPhysicalFullKeyboard(id) } to change
        }
            .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
            .shareIn(
                scope = applicationScope,
                started = SharingStarted.Lazily,
                replay = 1,
            )

    @FlowPreview
    override val newlyConnectedKeyboard: Flow<Keyboard> =
+9 −1
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package com.android.systemui.keyboard.data.repository
import android.hardware.input.InputManager
import android.hardware.input.InputManager.KeyboardBacklightListener
import android.hardware.input.KeyboardBacklightState
import android.testing.TestableLooper
import android.view.InputDevice
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,11 +28,13 @@ 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.inputdevice.data.repository.InputDeviceRepository
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
import com.android.systemui.util.mockito.whenever
import com.android.systemui.utils.os.FakeHandler
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -53,6 +56,7 @@ import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@TestableLooper.RunWithLooper
@RunWith(AndroidJUnit4::class)
class KeyboardRepositoryTest : SysuiTestCase() {

@@ -63,6 +67,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {

    private lateinit var underTest: KeyboardRepository
    private lateinit var dispatcher: CoroutineDispatcher
    private lateinit var inputDeviceRepo: InputDeviceRepository
    private lateinit var testScope: TestScope

    @Before
@@ -75,7 +80,9 @@ class KeyboardRepositoryTest : SysuiTestCase() {
        }
        dispatcher = StandardTestDispatcher()
        testScope = TestScope(dispatcher)
        underTest = KeyboardRepositoryImpl(testScope.backgroundScope, dispatcher, inputManager)
        val handler = FakeHandler(TestableLooper.get(this).looper)
        inputDeviceRepo = InputDeviceRepository(handler, testScope.backgroundScope, inputManager)
        underTest = KeyboardRepositoryImpl(dispatcher, inputManager, inputDeviceRepo)
    }

    @Test
@@ -363,6 +370,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
        private val maxBrightnessLevel: Int
    ) : KeyboardBacklightState() {
        override fun getBrightnessLevel() = brightnessLevel

        override fun getMaxBrightnessLevel() = maxBrightnessLevel
    }
}