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.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Application 27 import com.android.systemui.dagger.qualifiers.Background 28 import com.android.systemui.keyboard.data.model.Keyboard 29 import com.android.systemui.keyboard.shared.model.BacklightModel 30 import java.util.concurrent.Executor 31 import javax.inject.Inject 32 import kotlinx.coroutines.CoroutineDispatcher 33 import kotlinx.coroutines.CoroutineScope 34 import kotlinx.coroutines.FlowPreview 35 import kotlinx.coroutines.channels.SendChannel 36 import kotlinx.coroutines.channels.awaitClose 37 import kotlinx.coroutines.flow.Flow 38 import kotlinx.coroutines.flow.SharingStarted 39 import kotlinx.coroutines.flow.asFlow 40 import kotlinx.coroutines.flow.distinctUntilChanged 41 import kotlinx.coroutines.flow.emptyFlow 42 import kotlinx.coroutines.flow.flatMapConcat 43 import kotlinx.coroutines.flow.flowOf 44 import kotlinx.coroutines.flow.flowOn 45 import kotlinx.coroutines.flow.map 46 import kotlinx.coroutines.flow.mapNotNull 47 import kotlinx.coroutines.flow.shareIn 48 49 /** 50 * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be 51 * useful command line-driven implementation during development. 52 */ 53 interface KeyboardRepository { 54 /** Emits true if any physical keyboard is connected to the device, false otherwise. */ 55 val isAnyKeyboardConnected: Flow<Boolean> 56 57 /** 58 * Emits [Keyboard] object whenever new physical keyboard connects. When SysUI (re)starts it 59 * emits all currently connected keyboards 60 */ 61 val newlyConnectedKeyboard: Flow<Keyboard> 62 63 /** 64 * Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only 65 * happen when physical keyboard is connected 66 */ 67 val backlight: Flow<BacklightModel> 68 } 69 70 @SysUISingleton 71 class KeyboardRepositoryImpl 72 @Inject 73 constructor( 74 @Application private val applicationScope: CoroutineScope, 75 @Background private val backgroundDispatcher: CoroutineDispatcher, 76 private val inputManager: InputManager, 77 ) : KeyboardRepository { 78 79 private sealed interface DeviceChange 80 private data class DeviceAdded(val deviceId: Int) : DeviceChange 81 private object DeviceRemoved : DeviceChange 82 private object FreshStart : DeviceChange 83 84 /** 85 * Emits collection of all currently connected keyboards and what was the last [DeviceChange]. 86 * It emits collection so that every new subscriber to this SharedFlow can get latest state of 87 * all keyboards. Otherwise we might get into situation where subscriber timing on 88 * initialization matter and later subscriber will only get latest device and will miss all 89 * previous devices. 90 */ 91 private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> = <lambda>null92 conflatedCallbackFlow { 93 var connectedDevices = inputManager.inputDeviceIds.toSet() 94 val listener = 95 object : InputManager.InputDeviceListener { 96 override fun onInputDeviceAdded(deviceId: Int) { 97 connectedDevices = connectedDevices + deviceId 98 sendWithLogging(connectedDevices to DeviceAdded(deviceId)) 99 } 100 101 override fun onInputDeviceChanged(deviceId: Int) = Unit 102 103 override fun onInputDeviceRemoved(deviceId: Int) { 104 connectedDevices = connectedDevices - deviceId 105 sendWithLogging(connectedDevices to DeviceRemoved) 106 } 107 } 108 sendWithLogging(connectedDevices to FreshStart) 109 inputManager.registerInputDeviceListener(listener, /* handler= */ null) 110 awaitClose { inputManager.unregisterInputDeviceListener(listener) } 111 } changenull112 .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change } 113 .shareIn( 114 scope = applicationScope, 115 started = SharingStarted.Lazily, 116 replay = 1, 117 ) 118 119 @FlowPreview 120 override val newlyConnectedKeyboard: Flow<Keyboard> = 121 keyboardsChange devicesnull122 .flatMapConcat { (devices, operation) -> 123 when (operation) { 124 FreshStart -> devices.asFlow() 125 is DeviceAdded -> flowOf(operation.deviceId) 126 is DeviceRemoved -> emptyFlow() 127 } 128 } <lambda>null129 .mapNotNull { deviceIdToKeyboard(it) } 130 .flowOn(backgroundDispatcher) 131 132 override val isAnyKeyboardConnected: Flow<Boolean> = 133 keyboardsChange devicesnull134 .map { (devices, _) -> devices.isNotEmpty() } 135 .distinctUntilChanged() 136 .flowOn(backgroundDispatcher) 137 <lambda>null138 private val backlightStateListener: Flow<KeyboardBacklightState> = conflatedCallbackFlow { 139 val listener = KeyboardBacklightListener { _, state, isTriggeredByKeyPress -> 140 if (isTriggeredByKeyPress) { 141 sendWithLogging(state) 142 } 143 } 144 inputManager.registerKeyboardBacklightListener(Executor(Runnable::run), listener) 145 awaitClose { inputManager.unregisterKeyboardBacklightListener(listener) } 146 } 147 deviceIdToKeyboardnull148 private fun deviceIdToKeyboard(deviceId: Int): Keyboard? { 149 val device = inputManager.getInputDevice(deviceId) ?: return null 150 return Keyboard(device.vendorId, device.productId) 151 } 152 153 override val backlight: Flow<BacklightModel> = 154 backlightStateListener <lambda>null155 .map { BacklightModel(it.brightnessLevel, it.maxBrightnessLevel) } 156 .flowOn(backgroundDispatcher) 157 sendWithLoggingnull158 private fun <T> SendChannel<T>.sendWithLogging(element: T) { 159 trySendWithFailureLogging(element, TAG) 160 } 161 isPhysicalFullKeyboardnull162 private fun isPhysicalFullKeyboard(deviceId: Int): Boolean { 163 val device = inputManager.getInputDevice(deviceId) ?: return false 164 return !device.isVirtual && device.isFullKeyboard 165 } 166 167 companion object { 168 const val TAG = "KeyboardRepositoryImpl" 169 } 170 } 171