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