• 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.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