diff options
3 files changed, 114 insertions, 45 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt new file mode 100644 index 000000000000..3b161b659af9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt @@ -0,0 +1,95 @@ +/* + * 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" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index 91d528074723..817849c41297 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -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) } - } - .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change } - .shareIn( - scope = applicationScope, - started = SharingStarted.Lazily, - replay = 1, - ) + inputDeviceRepository.deviceChange.map { (ids, change) -> + ids.filter { id -> isPhysicalFullKeyboard(id) } to change + } @FlowPreview override val newlyConnectedKeyboard: Flow<Keyboard> = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt index 53bcf865b829..361e768a5b51 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt @@ -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 } } |