1 /* 2 * 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 interface KeyboardRepository { 50 /** Emits true if any physical keyboard is connected to the device, false otherwise. */ 51 val isAnyKeyboardConnected: Flow<Boolean> 52 53 /** 54 * Emits [Keyboard] object whenever new physical keyboard connects. When SysUI (re)starts it 55 * emits all currently connected keyboards 56 */ 57 val newlyConnectedKeyboard: Flow<Keyboard> 58 59 /** 60 * Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only 61 * happen when physical keyboard is connected 62 */ 63 val backlight: Flow<BacklightModel> 64 } 65 66 @SysUISingleton 67 class KeyboardRepositoryImpl 68 @Inject 69 constructor( 70 @Application private val applicationScope: CoroutineScope, 71 @Background private val backgroundDispatcher: CoroutineDispatcher, 72 private val inputManager: InputManager, 73 ) : KeyboardRepository { 74 75 private sealed interface DeviceChange 76 private data class DeviceAdded(val deviceId: Int) : DeviceChange 77 private object DeviceRemoved : DeviceChange 78 private object FreshStart : DeviceChange 79 80 /** 81 * Emits collection of all currently connected keyboards and what was the last [DeviceChange]. 82 * It emits collection so that every new subscriber to this SharedFlow can get latest state of 83 * all keyboards. Otherwise we might get into situation where subscriber timing on 84 * initialization matter and later subscriber will only get latest device and will miss all 85 * previous devices. 86 */ 87 private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> = 88 conflatedCallbackFlow { 89 var connectedDevices = inputManager.inputDeviceIds.toSet() 90 val listener = 91 object : InputManager.InputDeviceListener { 92 override fun onInputDeviceAdded(deviceId: Int) { 93 connectedDevices = connectedDevices + deviceId 94 sendWithLogging(connectedDevices to DeviceAdded(deviceId)) 95 } 96 97 override fun onInputDeviceChanged(deviceId: Int) = Unit 98 99 override fun onInputDeviceRemoved(deviceId: Int) { 100 connectedDevices = connectedDevices - deviceId 101 sendWithLogging(connectedDevices to DeviceRemoved) 102 } 103 } 104 sendWithLogging(connectedDevices to FreshStart) 105 inputManager.registerInputDeviceListener(listener, /* handler= */ null) 106 awaitClose { inputManager.unregisterInputDeviceListener(listener) } 107 } 108 .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change } 109 .shareIn( 110 scope = applicationScope, 111 started = SharingStarted.Lazily, 112 replay = 1, 113 ) 114 115 @FlowPreview 116 override val newlyConnectedKeyboard: Flow<Keyboard> = 117 keyboardsChange 118 .flatMapConcat { (devices, operation) -> 119 when (operation) { 120 FreshStart -> devices.asFlow() 121 is DeviceAdded -> flowOf(operation.deviceId) 122 is DeviceRemoved -> emptyFlow() 123 } 124 } 125 .mapNotNull { deviceIdToKeyboard(it) } 126 .flowOn(backgroundDispatcher) 127 128 override val isAnyKeyboardConnected: Flow<Boolean> = 129 keyboardsChange 130 .map { (devices, _) -> devices.isNotEmpty() } 131 .distinctUntilChanged() 132 .flowOn(backgroundDispatcher) 133 134 private val backlightStateListener: Flow<KeyboardBacklightState> = conflatedCallbackFlow { 135 val listener = KeyboardBacklightListener { _, state, isTriggeredByKeyPress -> 136 if (isTriggeredByKeyPress) { 137 sendWithLogging(state) 138 } 139 } 140 inputManager.registerKeyboardBacklightListener(Executor(Runnable::run), listener) 141 awaitClose { inputManager.unregisterKeyboardBacklightListener(listener) } 142 } 143 144 private fun deviceIdToKeyboard(deviceId: Int): Keyboard? { 145 val device = inputManager.getInputDevice(deviceId) ?: return null 146 return Keyboard(device.vendorId, device.productId) 147 } 148 149 override val backlight: Flow<BacklightModel> = 150 backlightStateListener 151 .map { BacklightModel(it.brightnessLevel, it.maxBrightnessLevel) } 152 .flowOn(backgroundDispatcher) 153 154 private fun <T> SendChannel<T>.sendWithLogging(element: T) { 155 trySendWithFailureLogging(element, TAG) 156 } 157 158 private fun isPhysicalFullKeyboard(deviceId: Int): Boolean { 159 val device = inputManager.getInputDevice(deviceId) ?: return false 160 return !device.isVirtual && device.isFullKeyboard 161 } 162 163 companion object { 164 const val TAG = "KeyboardRepositoryImpl" 165 } 166 } 167