• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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 package com.android.systemui.keyboard.shortcut.data.repository
18 
19 import android.hardware.input.InputGestureData
20 import android.hardware.input.InputGestureData.Builder
21 import android.hardware.input.InputGestureData.KeyTrigger
22 import android.hardware.input.InputGestureData.Trigger
23 import android.hardware.input.InputGestureData.createKeyTrigger
24 import android.hardware.input.InputManager
25 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION
26 import android.hardware.input.KeyGestureEvent.KeyGestureType
27 import android.hardware.input.KeyGlyphMap
28 import android.util.Log
29 import androidx.annotation.VisibleForTesting
30 import com.android.systemui.Flags.appShortcutRemovalFix
31 import com.android.systemui.Flags.shortcutHelperKeyGlyph
32 import com.android.systemui.dagger.SysUISingleton
33 import com.android.systemui.dagger.qualifiers.Background
34 import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
35 import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
36 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
37 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
38 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
39 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization
40 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete
41 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
42 import javax.inject.Inject
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.flow.Flow
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.SharingStarted
47 import kotlinx.coroutines.flow.combine
48 import kotlinx.coroutines.flow.stateIn
49 
50 @SysUISingleton
51 class CustomShortcutCategoriesRepository
52 @Inject
53 constructor(
54     private val inputDeviceRepository: ShortcutHelperInputDeviceRepository,
55     @Background private val backgroundScope: CoroutineScope,
56     private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
57     private val inputGestureDataAdapter: InputGestureDataAdapter,
58     private val customInputGesturesRepository: CustomInputGesturesRepository,
59     private val inputManager: InputManager,
60     private val appLaunchDataRepository: AppLaunchDataRepository,
61 ) : ShortcutCategoriesRepository {
62 
63     private val _selectedKeyCombination = MutableStateFlow<KeyCombination?>(null)
64     private val _shortcutBeingCustomized = MutableStateFlow<ShortcutCustomizationRequestInfo?>(null)
65 
66     val pressedKeys =
67         _selectedKeyCombination
68             .combine(inputDeviceRepository.activeInputDevice) { keyCombination, inputDevice ->
69                 if (inputDevice == null || keyCombination == null) {
70                     return@combine emptyList()
71                 } else {
72                     val keyGlyphMap = getKeyGlyphMap(inputDevice.id)
73                     val modifiers =
74                         shortcutCategoriesUtils.toShortcutModifierKeys(
75                             keyCombination.modifiers,
76                             keyGlyphMap,
77                         )
78                     val triggerKey =
79                         keyCombination.keyCode?.let {
80                             shortcutCategoriesUtils.toShortcutKey(
81                                 keyGlyphMap,
82                                 inputDevice.keyCharacterMap,
83                                 keyCode = it,
84                             )
85                         }
86                     val keys = mutableListOf<ShortcutKey>()
87                     modifiers?.let { keys += it }
88                     triggerKey?.let { keys += it }
89                     return@combine keys
90                 }
91             }
92             .stateIn(
93                 scope = backgroundScope,
94                 started = SharingStarted.Lazily,
95                 initialValue = emptyList(),
96             )
97 
98     override val categories: Flow<List<ShortcutCategory>> =
99         combine(
100                 inputDeviceRepository.activeInputDevice,
101                 customInputGesturesRepository.customInputGestures,
102             ) { inputDevice, inputGestures ->
103                 if (inputDevice == null) {
104                     emptyList()
105                 } else {
106                     val sources = inputGestureDataAdapter.toInternalGroupSources(inputGestures)
107                     val supportedKeyCodes =
108                         shortcutCategoriesUtils.fetchSupportedKeyCodes(
109                             inputDevice.id,
110                             sources.map { it.groups },
111                         )
112 
113                     val result =
114                         sources.mapNotNull { source ->
115                             shortcutCategoriesUtils.fetchShortcutCategory(
116                                 type = source.type,
117                                 groups = source.groups,
118                                 inputDevice = inputDevice,
119                                 supportedKeyCodes = supportedKeyCodes,
120                             )
121                         }
122                     result
123                 }
124             }
125             .stateIn(
126                 scope = backgroundScope,
127                 initialValue = emptyList(),
128                 started = SharingStarted.Lazily,
129             )
130 
131     fun updateUserKeyCombination(keyCombination: KeyCombination?) {
132         _selectedKeyCombination.value = keyCombination
133     }
134 
135     fun onCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo?) {
136         _shortcutBeingCustomized.value = requestInfo
137     }
138 
139     @VisibleForTesting
140     fun buildInputGestureDataForShortcutBeingCustomized(): InputGestureData? {
141         try {
142             return Builder()
143                 .addKeyGestureTypeForShortcutBeingCustomized()
144                 .addTriggerFromSelectedKeyCombination()
145                 .addAppLaunchDataFromShortcutBeingCustomized()
146                 .build()
147         } catch (e: IllegalArgumentException) {
148             Log.w(TAG, "could not add custom shortcut: $e")
149             return null
150         }
151     }
152 
153     private fun retrieveInputGestureDataForShortcutBeingDeleted(): InputGestureData? {
154         val keyGestureTypeForShortcutBeingDeleted = getKeyGestureTypeForShortcutBeingCustomized()
155         if (appShortcutRemovalFix()) {
156             val inputGesturesMatchingKeyGestureType =
157                 customInputGesturesRepository.retrieveCustomInputGestures().filter {
158                     it.action.keyGestureType() == keyGestureTypeForShortcutBeingDeleted
159                 }
160 
161             return if (
162                 keyGestureTypeForShortcutBeingDeleted == KEY_GESTURE_TYPE_LAUNCH_APPLICATION
163             ) {
164                 val shortcutBeingDeleted = getShortcutBeingCustomized() as Delete
165                 if (shortcutBeingDeleted.customShortcutCommand == null) {
166                     Log.w(
167                         TAG,
168                         "Requested to delete custom shortcut but customShortcutCommand was null",
169                     )
170                     return null
171                 }
172 
173                 inputGesturesMatchingKeyGestureType.firstOrNull {
174                     checkShortcutKeyTriggerEquality(
175                         it.trigger,
176                         shortcutBeingDeleted.customShortcutCommand.keys,
177                     ) ?: false
178                 }
179             } else {
180                 inputGesturesMatchingKeyGestureType.firstOrNull()
181             }
182         } else {
183             return customInputGesturesRepository.retrieveCustomInputGestures().firstOrNull {
184                 it.action.keyGestureType() == keyGestureTypeForShortcutBeingDeleted
185             }
186         }
187     }
188 
189     suspend fun confirmAndSetShortcutCurrentlyBeingCustomized():
190         ShortcutCustomizationRequestResult {
191         val inputGestureData =
192             buildInputGestureDataForShortcutBeingCustomized()
193                 ?: return ShortcutCustomizationRequestResult.ERROR_OTHER
194 
195         return customInputGesturesRepository.addCustomInputGesture(inputGestureData)
196     }
197 
198     suspend fun deleteShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult {
199         val inputGestureData =
200             retrieveInputGestureDataForShortcutBeingDeleted()
201                 ?: return ShortcutCustomizationRequestResult.ERROR_OTHER
202         return customInputGesturesRepository.deleteCustomInputGesture(inputGestureData)
203     }
204 
205     suspend fun resetAllCustomShortcuts(): ShortcutCustomizationRequestResult {
206         return customInputGesturesRepository.resetAllCustomInputGestures()
207     }
208 
209     suspend fun isSelectedKeyCombinationAvailable(): Boolean {
210         val trigger = buildTriggerFromSelectedKeyCombination() ?: return false
211         return customInputGesturesRepository.getInputGestureByTrigger(trigger) == null
212     }
213 
214     private fun checkShortcutKeyTriggerEquality(
215         trigger: Trigger,
216         keys: List<ShortcutKey>,
217     ): Boolean? {
218         return getConvertedKeyTrigger(trigger)?.containsAll(keys)
219     }
220 
221     private fun getConvertedKeyTrigger(trigger: Trigger): List<ShortcutKey>? {
222         if (trigger is KeyTrigger) {
223             val inputDevice = inputDeviceRepository.activeInputDevice.value ?: return null
224 
225             val modifierKeys =
226                 shortcutCategoriesUtils.toShortcutModifierKeys(
227                     keyGlyphMap = getKeyGlyphMap(inputDevice.id),
228                     modifiers = trigger.modifierState,
229                 ) ?: return null
230 
231             val keyCodeShortcutKey =
232                 shortcutCategoriesUtils.toShortcutKey(
233                     keyGlyphMap = getKeyGlyphMap(inputDevice.id),
234                     keyCharacterMap = inputDevice.keyCharacterMap,
235                     keyCode = trigger.keycode,
236                 ) ?: return null
237 
238             return modifierKeys + keyCodeShortcutKey
239         }
240         return null
241     }
242 
243     private fun getKeyGlyphMap(deviceId: Int): KeyGlyphMap? {
244         return if (shortcutHelperKeyGlyph()) {
245             inputManager.getKeyGlyphMap(deviceId)
246         } else null
247     }
248 
249     private fun Builder.addKeyGestureTypeForShortcutBeingCustomized(): Builder {
250         val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized()
251 
252         if (keyGestureType == null) {
253             Log.w(
254                 TAG,
255                 "Could not find KeyGestureType for shortcut ${_shortcutBeingCustomized.value}",
256             )
257             return this
258         }
259         return setKeyGestureType(keyGestureType)
260     }
261 
262     private fun Builder.addAppLaunchDataFromShortcutBeingCustomized(): Builder {
263         val shortcutBeingCustomized =
264             (_shortcutBeingCustomized.value as? SingleShortcutCustomization) ?: return this
265 
266         if (shortcutBeingCustomized.categoryType != ShortcutCategoryType.AppCategories) {
267             return this
268         }
269 
270         val defaultShortcutCommand = shortcutBeingCustomized.defaultShortcutCommand ?: return this
271         val appLaunchData =
272             appLaunchDataRepository.getAppLaunchDataForShortcutWithCommand(defaultShortcutCommand)
273 
274         return if (appLaunchData == null) this else this.setAppLaunchData(appLaunchData)
275     }
276 
277     @KeyGestureType
278     private fun getKeyGestureTypeForShortcutBeingCustomized(): Int? {
279         val shortcutBeingCustomized = getShortcutBeingCustomized() as? SingleShortcutCustomization
280 
281         if (shortcutBeingCustomized == null) {
282             Log.w(
283                 TAG,
284                 "Requested key gesture type from label but shortcut being customized is null",
285             )
286             return null
287         }
288 
289         return inputGestureDataAdapter.getKeyGestureTypeForShortcut(
290             shortcutLabel = shortcutBeingCustomized.label,
291             shortcutCategoryType = shortcutBeingCustomized.categoryType,
292         )
293     }
294 
295     private fun Builder.addTriggerFromSelectedKeyCombination(): Builder =
296         setTrigger(buildTriggerFromSelectedKeyCombination())
297 
298     private fun buildTriggerFromSelectedKeyCombination(): Trigger? {
299         val selectedKeyCombination = _selectedKeyCombination.value
300         if (selectedKeyCombination?.keyCode == null) {
301             Log.w(
302                 TAG,
303                 "User requested to set shortcut but selected key combination is " +
304                     "$selectedKeyCombination",
305             )
306             return null
307         }
308 
309         return createKeyTrigger(
310             /* keycode= */ selectedKeyCombination.keyCode,
311             /* modifierState= */ shortcutCategoriesUtils.removeUnsupportedModifiers(
312                 selectedKeyCombination.modifiers
313             ),
314         )
315     }
316 
317     @VisibleForTesting
318     fun getShortcutBeingCustomized(): ShortcutCustomizationRequestInfo? {
319         return _shortcutBeingCustomized.value
320     }
321 
322     private companion object {
323         private const val TAG = "CustomShortcutCategoriesRepository"
324     }
325 }
326