1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.systemui.keyboard.data.repository 19 20 import android.hardware.input.InputManager 21 import android.hardware.input.InputManager.KeyboardBacklightListener 22 import android.hardware.input.KeyboardBacklightState 23 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dagger.qualifiers.Background 26 import com.android.systemui.inputdevice.data.repository.InputDeviceRepository 27 import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceAdded 28 import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceRemoved 29 import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.FreshStart 30 import com.android.systemui.keyboard.data.model.Keyboard 31 import com.android.systemui.keyboard.shared.model.BacklightModel 32 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 33 import java.util.concurrent.Executor 34 import javax.inject.Inject 35 import kotlinx.coroutines.CoroutineDispatcher 36 import kotlinx.coroutines.FlowPreview 37 import kotlinx.coroutines.channels.SendChannel 38 import kotlinx.coroutines.channels.awaitClose 39 import kotlinx.coroutines.flow.Flow 40 import kotlinx.coroutines.flow.asFlow 41 import kotlinx.coroutines.flow.distinctUntilChanged 42 import kotlinx.coroutines.flow.emptyFlow 43 import kotlinx.coroutines.flow.filter 44 import kotlinx.coroutines.flow.flatMapConcat 45 import kotlinx.coroutines.flow.flowOf 46 import kotlinx.coroutines.flow.flowOn 47 import kotlinx.coroutines.flow.map 48 import kotlinx.coroutines.flow.mapNotNull 49 50 /** 51 * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be 52 * useful command line-driven implementation during development. 53 */ 54 interface KeyboardRepository { 55 /** Emits true if any physical keyboard is connected to the device, false otherwise. */ 56 val isAnyKeyboardConnected: Flow<Boolean> 57 58 /** 59 * Emits [Keyboard] object whenever new physical keyboard connects. When SysUI (re)starts it 60 * emits all currently connected keyboards 61 */ 62 val newlyConnectedKeyboard: Flow<Keyboard> 63 64 /** Emits set of currently connected keyboards */ 65 val connectedKeyboards: Flow<Set<Keyboard>> 66 67 /** 68 * Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only 69 * happen when physical keyboard is connected 70 */ 71 val backlight: Flow<BacklightModel> 72 } 73 74 @SysUISingleton 75 class KeyboardRepositoryImpl 76 @Inject 77 constructor( 78 @Background private val backgroundDispatcher: CoroutineDispatcher, 79 private val inputManager: InputManager, 80 inputDeviceRepository: InputDeviceRepository, 81 ) : KeyboardRepository { 82 83 @FlowPreview 84 override val newlyConnectedKeyboard: Flow<Keyboard> = 85 inputDeviceRepository.deviceChange devicesnull86 .flatMapConcat { (devices, operation) -> 87 when (operation) { 88 FreshStart -> devices.filter { id -> isPhysicalFullKeyboard(id) }.asFlow() 89 is DeviceAdded -> { 90 if (isPhysicalFullKeyboard(operation.deviceId)) flowOf(operation.deviceId) 91 else emptyFlow() 92 } 93 is DeviceRemoved -> emptyFlow() 94 } 95 } <lambda>null96 .mapNotNull { deviceIdToKeyboard(it) } 97 .flowOn(backgroundDispatcher) 98 99 override val connectedKeyboards: Flow<Set<Keyboard>> = 100 inputDeviceRepository.deviceChange deviceIdsnull101 .map { (deviceIds, _) -> deviceIds } <lambda>null102 .map { deviceIds -> deviceIds.filter { isPhysicalFullKeyboard(it) } } 103 .distinctUntilChanged() <lambda>null104 .map { deviceIds -> deviceIds.mapNotNull { deviceIdToKeyboard(it) }.toSet() } 105 106 override val isAnyKeyboardConnected: Flow<Boolean> = 107 inputDeviceRepository.deviceChange idsnull108 .map { (ids, _) -> ids.any { id -> isPhysicalFullKeyboard(id) } } 109 .distinctUntilChanged() 110 .flowOn(backgroundDispatcher) 111 <lambda>null112 private val backlightStateListener: Flow<KeyboardBacklightState> = conflatedCallbackFlow { 113 val listener = KeyboardBacklightListener { _, state, isTriggeredByKeyPress -> 114 if (isTriggeredByKeyPress) { 115 sendWithLogging(state) 116 } 117 } 118 inputManager.registerKeyboardBacklightListener(Executor(Runnable::run), listener) 119 awaitClose { inputManager.unregisterKeyboardBacklightListener(listener) } 120 } 121 deviceIdToKeyboardnull122 private fun deviceIdToKeyboard(deviceId: Int): Keyboard? { 123 val device = inputManager.getInputDevice(deviceId) ?: return null 124 return Keyboard(device.vendorId, device.productId) 125 } 126 127 override val backlight: Flow<BacklightModel> = 128 backlightStateListener <lambda>null129 .map { BacklightModel(it.brightnessLevel, it.maxBrightnessLevel) } 130 .flowOn(backgroundDispatcher) 131 sendWithLoggingnull132 private fun <T> SendChannel<T>.sendWithLogging(element: T) { 133 trySendWithFailureLogging(element, TAG) 134 } 135 isPhysicalFullKeyboardnull136 private fun isPhysicalFullKeyboard(deviceId: Int): Boolean { 137 val device = inputManager.getInputDevice(deviceId) ?: return false 138 return !device.isVirtual && device.isFullKeyboard 139 } 140 141 companion object { 142 const val TAG = "KeyboardRepositoryImpl" 143 } 144 } 145