Loading packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } } packages/SystemUI/src/com/android/systemui/keyboard/data/model/Keyboard.kt 0 → 100644 +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) packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +63 −13 Original line number Diff line number Diff line Loading @@ -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> } Loading @@ -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) { Loading @@ -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 Loading packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt +4 −4 Original line number Diff line number Diff line Loading @@ -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)) Loading @@ -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() } Loading packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt +2 −2 Original line number Diff line number Diff line Loading @@ -58,7 +58,7 @@ class BacklightDialogViewModelTest : SysuiTestCase() { KeyboardBacklightInteractor(keyboardRepository), accessibilityManagerWrapper ) keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true) } @Test Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractor.kt +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } }
packages/SystemUI/src/com/android/systemui/keyboard/data/model/Keyboard.kt 0 → 100644 +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)
packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +63 −13 Original line number Diff line number Diff line Loading @@ -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> } Loading @@ -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) { Loading @@ -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 Loading
packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt +4 −4 Original line number Diff line number Diff line Loading @@ -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)) Loading @@ -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() } Loading
packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/viewmodel/BacklightDialogViewModelTest.kt +2 −2 Original line number Diff line number Diff line Loading @@ -58,7 +58,7 @@ class BacklightDialogViewModelTest : SysuiTestCase() { KeyboardBacklightInteractor(keyboardRepository), accessibilityManagerWrapper ) keyboardRepository.setKeyboardConnected(true) keyboardRepository.setIsAnyKeyboardConnected(true) } @Test Loading @@ -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