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.ui.viewmodel 18 19 import android.content.Context 20 import androidx.compose.ui.input.key.Key 21 import androidx.compose.ui.input.key.KeyEvent 22 import androidx.compose.ui.input.key.KeyEventType 23 import androidx.compose.ui.input.key.isMetaPressed 24 import androidx.compose.ui.input.key.key 25 import androidx.compose.ui.input.key.nativeKeyCode 26 import androidx.compose.ui.input.key.type 27 import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult 28 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor 29 import com.android.systemui.keyboard.shortcut.extensions.toContentDescription 30 import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination 31 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo 32 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey 33 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState 34 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.AddShortcutDialog 35 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.DeleteShortcutDialog 36 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.ResetShortcutDialog 37 import com.android.systemui.lifecycle.ExclusiveActivatable 38 import com.android.systemui.res.R 39 import dagger.assisted.AssistedFactory 40 import dagger.assisted.AssistedInject 41 import kotlinx.coroutines.flow.MutableStateFlow 42 import kotlinx.coroutines.flow.asStateFlow 43 import kotlinx.coroutines.flow.update 44 45 class ShortcutCustomizationViewModel 46 @AssistedInject 47 constructor( 48 private val context: Context, 49 private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, 50 ) : ExclusiveActivatable() { 51 private var keyDownEventCache: KeyEvent? = null 52 private val _shortcutCustomizationUiState = 53 MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) 54 55 val shortcutCustomizationUiState = _shortcutCustomizationUiState.asStateFlow() 56 57 fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) { 58 shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) 59 when (requestInfo) { 60 is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> { 61 _shortcutCustomizationUiState.value = 62 AddShortcutDialog( 63 shortcutLabel = requestInfo.label, 64 defaultCustomShortcutModifierKey = 65 shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(), 66 pressedKeys = emptyList(), 67 ) 68 } 69 70 is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> { 71 _shortcutCustomizationUiState.value = DeleteShortcutDialog 72 } 73 74 ShortcutCustomizationRequestInfo.Reset -> { 75 _shortcutCustomizationUiState.value = ResetShortcutDialog 76 } 77 } 78 } 79 80 fun onDialogDismissed() { 81 _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive 82 shortcutCustomizationInteractor.onCustomizationRequested(null) 83 clearSelectedKeyCombination() 84 } 85 86 fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { 87 if (isModifier(keyEvent)) { 88 return false 89 } 90 if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) { 91 keyDownEventCache = keyEvent 92 return true 93 } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) { 94 updatePressedKeys(keyDownEventCache!!) 95 clearKeyDownEventCache() 96 return true 97 } 98 return false 99 } 100 101 suspend fun onSetShortcut() { 102 val result = shortcutCustomizationInteractor.confirmAndSetShortcutCurrentlyBeingCustomized() 103 _shortcutCustomizationUiState.update { uiState -> 104 when (result) { 105 ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive 106 ShortcutCustomizationRequestResult.ERROR_RESERVED_COMBINATION -> { 107 getUiStateWithErrorMessage( 108 uiState = uiState, 109 errorMessage = 110 context.getString( 111 R.string.shortcut_customizer_key_combination_in_use_error_message 112 ), 113 ) 114 } 115 116 ShortcutCustomizationRequestResult.ERROR_OTHER -> 117 getUiStateWithErrorMessage( 118 uiState = uiState, 119 errorMessage = 120 context.getString(R.string.shortcut_customizer_generic_error_message), 121 ) 122 } 123 } 124 } 125 126 suspend fun deleteShortcutCurrentlyBeingCustomized() { 127 val result = shortcutCustomizationInteractor.deleteShortcutCurrentlyBeingCustomized() 128 129 _shortcutCustomizationUiState.update { uiState -> 130 when (result) { 131 ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive 132 else -> uiState 133 } 134 } 135 } 136 137 suspend fun resetAllCustomShortcuts() { 138 val result = shortcutCustomizationInteractor.resetAllCustomShortcuts() 139 140 _shortcutCustomizationUiState.update { uiState -> 141 when (result) { 142 ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive 143 else -> uiState 144 } 145 } 146 } 147 148 fun clearSelectedKeyCombination() { 149 shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) 150 } 151 152 private fun getUiStateWithErrorMessage( 153 uiState: ShortcutCustomizationUiState, 154 errorMessage: String, 155 ): ShortcutCustomizationUiState { 156 return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState 157 } 158 159 private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key) 160 161 private fun updatePressedKeys(keyEvent: KeyEvent) { 162 val keyCombination = 163 KeyCombination( 164 modifiers = keyEvent.nativeKeyEvent.modifiers, 165 keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null, 166 ) 167 shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination) 168 } 169 170 private fun clearKeyDownEventCache() { 171 keyDownEventCache = null 172 } 173 174 private suspend fun isSelectedKeyCombinationAvailable() = 175 shortcutCustomizationInteractor.isSelectedKeyCombinationAvailable() 176 177 @AssistedFactory 178 interface Factory { 179 fun create(): ShortcutCustomizationViewModel 180 } 181 182 override suspend fun onActivated(): Nothing { 183 shortcutCustomizationInteractor.pressedKeys.collect { 184 val keys = filterDefaultCustomShortcutModifierKey(it) 185 val errorMessage = getErrorMessageForPressedKeys(keys) 186 187 _shortcutCustomizationUiState.update { uiState -> 188 if (uiState is AddShortcutDialog) { 189 uiState.copy( 190 pressedKeys = keys, 191 errorMessage = errorMessage, 192 pressedKeysDescription = getAccessibilityDescForPressedKeys(keys), 193 ) 194 } else { 195 uiState 196 } 197 } 198 } 199 } 200 201 private fun getAccessibilityDescForPressedKeys(keys: List<ShortcutKey>): String { 202 val andConjunction = 203 context.getString(R.string.shortcut_helper_key_combinations_and_conjunction) 204 return buildString { 205 keys.forEach { key -> 206 key.toContentDescription(context)?.let { 207 if (isNotEmpty()) { 208 append(", $andConjunction ") 209 } 210 append(it) 211 } 212 } 213 } 214 } 215 216 private suspend fun getErrorMessageForPressedKeys(keys: List<ShortcutKey>): String { 217 return if (keys.isEmpty() or isSelectedKeyCombinationAvailable()) { 218 "" 219 } else { 220 context.getString(R.string.shortcut_customizer_key_combination_in_use_error_message) 221 } 222 } 223 224 private fun filterDefaultCustomShortcutModifierKey(keys: List<ShortcutKey>) = 225 keys.filter { it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey() } 226 227 companion object { 228 private val SUPPORTED_MODIFIERS = 229 listOf( 230 Key.MetaLeft, 231 Key.MetaRight, 232 Key.CtrlRight, 233 Key.CtrlLeft, 234 Key.AltLeft, 235 Key.AltRight, 236 Key.ShiftLeft, 237 Key.ShiftRight, 238 Key.Function, 239 Key.Symbol, 240 ) 241 } 242 } 243