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