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