• 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.content.Context
20 import android.graphics.drawable.Icon
21 import android.hardware.input.InputGestureData.KeyTrigger
22 import android.hardware.input.InputManager
23 import android.hardware.input.KeyGlyphMap
24 import android.util.Log
25 import android.view.InputDevice
26 import android.view.KeyCharacterMap
27 import android.view.KeyEvent
28 import android.view.KeyEvent.META_META_ON
29 import com.android.systemui.Flags.shortcutHelperKeyGlyph
30 import com.android.systemui.dagger.qualifiers.Background
31 import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
32 import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
33 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
34 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
35 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
36 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
37 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperExclusions
38 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
39 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
40 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
41 import javax.inject.Inject
42 import kotlin.coroutines.CoroutineContext
43 import kotlinx.coroutines.withContext
44 
45 class ShortcutCategoriesUtils
46 @Inject
47 constructor(
48     private val context: Context,
49     @Background private val backgroundCoroutineContext: CoroutineContext,
50     private val inputManager: InputManager,
51     private val shortcutHelperExclusions: ShortcutHelperExclusions,
52 ) {
53 
54     fun removeUnsupportedModifiers(modifierMask: Int): Int {
55         return SUPPORTED_MODIFIERS.reduce { acc, modifier -> acc or modifier } and modifierMask
56     }
57 
58     fun fetchShortcutCategory(
59         type: ShortcutCategoryType?,
60         groups: List<InternalKeyboardShortcutGroup>,
61         inputDevice: InputDevice,
62         supportedKeyCodes: Set<Int>,
63     ): ShortcutCategory? {
64         return if (type == null) {
65             null
66         } else {
67             val keyGlyphMap =
68                 if (shortcutHelperKeyGlyph()) inputManager.getKeyGlyphMap(inputDevice.id) else null
69             toShortcutCategory(
70                 keyGlyphMap,
71                 inputDevice.keyCharacterMap,
72                 type,
73                 groups,
74                 type.isTrusted,
75                 supportedKeyCodes,
76             )
77         }
78     }
79 
80     private fun toShortcutCategory(
81         keyGlyphMap: KeyGlyphMap?,
82         keyCharacterMap: KeyCharacterMap,
83         type: ShortcutCategoryType,
84         shortcutGroups: List<InternalKeyboardShortcutGroup>,
85         keepIcons: Boolean,
86         supportedKeyCodes: Set<Int>,
87     ): ShortcutCategory? {
88         val subCategories =
89             shortcutGroups
90                 .map { shortcutGroup ->
91                     ShortcutSubCategory(
92                         shortcutGroup.label,
93                         toShortcuts(
94                             keyGlyphMap,
95                             keyCharacterMap,
96                             shortcutGroup.items,
97                             keepIcons,
98                             supportedKeyCodes,
99                         ),
100                     )
101                 }
102                 .filter { it.shortcuts.isNotEmpty() }
103         return if (subCategories.isEmpty()) {
104             Log.w(TAG, "Empty sub categories after converting $shortcutGroups")
105             null
106         } else {
107             ShortcutCategory(type, subCategories)
108         }
109     }
110 
111     private fun toShortcuts(
112         keyGlyphMap: KeyGlyphMap?,
113         keyCharacterMap: KeyCharacterMap,
114         infoList: List<InternalKeyboardShortcutInfo>,
115         keepIcons: Boolean,
116         supportedKeyCodes: Set<Int>,
117     ) =
118         infoList
119             .filter {
120                 // Allow KEYCODE_UNKNOWN (0) because shortcuts can have just modifiers and no
121                 // keycode, or they could have a baseCharacter instead of a keycode.
122                 it.keycode == KeyEvent.KEYCODE_UNKNOWN ||
123                     supportedKeyCodes.contains(it.keycode) ||
124                     // Support keyboard function row key codes
125                     keyGlyphMap?.functionRowKeys?.contains(it.keycode) ?: false
126             }
127             .mapNotNull { toShortcut(keyGlyphMap, keyCharacterMap, it, keepIcons) }
128 
129     private fun toShortcut(
130         keyGlyphMap: KeyGlyphMap?,
131         keyCharacterMap: KeyCharacterMap,
132         shortcutInfo: InternalKeyboardShortcutInfo,
133         keepIcon: Boolean,
134     ): Shortcut? {
135         val shortcutCommand =
136             toShortcutCommand(keyGlyphMap, keyCharacterMap, shortcutInfo) ?: return null
137         return Shortcut(
138             label = shortcutInfo.label,
139             icon = toShortcutIcon(keepIcon, shortcutInfo),
140             commands = listOf(shortcutCommand),
141             isCustomizable = shortcutHelperExclusions.isShortcutCustomizable(shortcutInfo.label),
142         )
143     }
144 
145     private fun toShortcutIcon(
146         keepIcon: Boolean,
147         shortcutInfo: InternalKeyboardShortcutInfo,
148     ): ShortcutIcon? {
149         if (!keepIcon) {
150             return null
151         }
152         val icon = shortcutInfo.icon ?: return null
153         // For now only keep icons of type resource, which is what the "default apps" shortcuts
154         // provide.
155         if (icon.type != Icon.TYPE_RESOURCE || icon.resPackage.isNullOrEmpty() || icon.resId <= 0) {
156             return null
157         }
158         return ShortcutIcon(packageName = icon.resPackage, resourceId = icon.resId)
159     }
160 
161     fun toShortcutCommand(
162         keyGlyphMap: KeyGlyphMap?,
163         keyCharacterMap: KeyCharacterMap,
164         keyTrigger: KeyTrigger,
165     ): ShortcutCommand? {
166         return toShortcutCommand(
167             keyGlyphMap = keyGlyphMap,
168             keyCharacterMap = keyCharacterMap,
169             info =
170                 InternalKeyboardShortcutInfo(
171                     keycode = keyTrigger.keycode,
172                     modifiers = keyTrigger.modifierState,
173                 ),
174         )
175     }
176 
177     private fun toShortcutCommand(
178         keyGlyphMap: KeyGlyphMap?,
179         keyCharacterMap: KeyCharacterMap,
180         info: InternalKeyboardShortcutInfo,
181     ): ShortcutCommand? {
182         val keys = mutableListOf<ShortcutKey>()
183         var remainingModifiers = info.modifiers
184         SUPPORTED_MODIFIERS.forEach { supportedModifier ->
185             if ((supportedModifier and remainingModifiers) != 0) {
186                 keys += toShortcutModifierKey(keyGlyphMap, supportedModifier) ?: return null
187                 // "Remove" the modifier from the remaining modifiers
188                 remainingModifiers = remainingModifiers and supportedModifier.inv()
189             }
190         }
191         if (remainingModifiers != 0) {
192             // There is a remaining modifier we don't support
193             Log.w(TAG, "Unsupported modifiers remaining: $remainingModifiers")
194             return null
195         }
196         if (info.keycode != 0 || info.baseCharacter > Char.MIN_VALUE) {
197             keys +=
198                 toShortcutKey(keyGlyphMap, keyCharacterMap, info.keycode, info.baseCharacter)
199                     ?: return null
200         }
201         if (keys.isEmpty()) {
202             Log.w(TAG, "No keys for $info")
203             return null
204         }
205         return ShortcutCommand(keys = keys, isCustom = info.isCustomShortcut)
206     }
207 
208     fun toShortcutModifierKeys(modifiers: Int, keyGlyphMap: KeyGlyphMap?): List<ShortcutKey>? {
209         val keys: MutableList<ShortcutKey> = mutableListOf()
210         var remainingModifiers = modifiers
211         SUPPORTED_MODIFIERS.forEach { supportedModifier ->
212             if ((supportedModifier and remainingModifiers) != 0) {
213                 keys += toShortcutModifierKey(keyGlyphMap, supportedModifier) ?: return null
214                 remainingModifiers = remainingModifiers and supportedModifier.inv()
215             }
216         }
217         return keys
218     }
219 
220     private fun toShortcutModifierKey(keyGlyphMap: KeyGlyphMap?, modifierMask: Int): ShortcutKey? {
221         val modifierDrawable = keyGlyphMap?.getDrawableForModifierState(context, modifierMask)
222         if (modifierDrawable != null) {
223             return ShortcutKey.Icon.DrawableIcon(drawable = modifierDrawable)
224         }
225 
226         if (modifierMask == META_META_ON) {
227             return ShortcutKey.Icon.ResIdIcon(ShortcutHelperKeys.metaModifierIconResId)
228         }
229 
230         val modifierLabel = ShortcutHelperKeys.modifierLabels[modifierMask]
231         if (modifierLabel != null) {
232             return ShortcutKey.Text(modifierLabel(context))
233         }
234         Log.wtf("TAG", "Couldn't find label or icon for modifier $modifierMask")
235         return null
236     }
237 
238     fun toShortcutKey(
239         keyGlyphMap: KeyGlyphMap?,
240         keyCharacterMap: KeyCharacterMap,
241         keyCode: Int,
242         baseCharacter: Char = Char.MIN_VALUE,
243     ): ShortcutKey? {
244         val keycodeDrawable = keyGlyphMap?.getDrawableForKeycode(context, keyCode)
245         if (keycodeDrawable != null) {
246             return ShortcutKey.Icon.DrawableIcon(drawable = keycodeDrawable)
247         }
248 
249         val iconResId = ShortcutHelperKeys.keyIcons[keyCode]
250         if (iconResId != null) {
251             return ShortcutKey.Icon.ResIdIcon(iconResId)
252         }
253         if (baseCharacter > Char.MIN_VALUE) {
254             return ShortcutKey.Text(baseCharacter.uppercase())
255         }
256         val specialKeyLabel = ShortcutHelperKeys.specialKeyLabels[keyCode]
257         if (specialKeyLabel != null) {
258             val label = specialKeyLabel(context)
259             return ShortcutKey.Text(label)
260         }
261         val displayLabelCharacter = keyCharacterMap.getDisplayLabel(keyCode)
262         if (displayLabelCharacter.code != 0) {
263             return ShortcutKey.Text(displayLabelCharacter.toString())
264         }
265         Log.w(TAG, "Couldn't find label or icon for key: $keyCode")
266         return null
267     }
268 
269     suspend fun fetchSupportedKeyCodes(
270         deviceId: Int,
271         groupsFromAllSources: List<List<InternalKeyboardShortcutGroup>>,
272     ): Set<Int> =
273         withContext(backgroundCoroutineContext) {
274             val allUsedKeyCodes =
275                 groupsFromAllSources
276                     .flatMap { groups -> groups.flatMap { group -> group.items } }
277                     .map { info -> info.keycode }
278                     .distinct()
279             val keyCodesSupported =
280                 inputManager.deviceHasKeys(deviceId, allUsedKeyCodes.toIntArray())
281             return@withContext allUsedKeyCodes
282                 .filterIndexed { index, _ -> keyCodesSupported[index] }
283                 .toSet()
284         }
285 
286     companion object {
287         private const val TAG = "ShortcutCategoriesUtils"
288 
289         private val SUPPORTED_MODIFIERS =
290             listOf(
291                 KeyEvent.META_META_ON,
292                 KeyEvent.META_CTRL_ON,
293                 KeyEvent.META_ALT_ON,
294                 KeyEvent.META_SHIFT_ON,
295                 KeyEvent.META_SYM_ON,
296                 KeyEvent.META_FUNCTION_ON,
297             )
298     }
299 }
300