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