• 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.app.role.RoleManager
20 import android.content.Context
21 import android.content.pm.PackageManager.NameNotFoundException
22 import android.util.Log
23 import androidx.compose.material.icons.Icons
24 import androidx.compose.material.icons.filled.AccessibilityNew
25 import androidx.compose.material.icons.filled.Android
26 import androidx.compose.material.icons.filled.Apps
27 import androidx.compose.material.icons.filled.Keyboard
28 import androidx.compose.material.icons.filled.Tv
29 import androidx.compose.material.icons.filled.VerticalSplit
30 import com.android.compose.ui.graphics.painter.DrawablePainter
31 import com.android.systemui.Flags.keyboardShortcutHelperShortcutCustomizer
32 import com.android.systemui.dagger.qualifiers.Background
33 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
34 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCustomizationModeInteractor
35 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
36 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
37 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
38 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
39 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.CurrentApp
40 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
41 import com.android.systemui.keyboard.shortcut.ui.model.IconSource
42 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi
43 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
44 import com.android.systemui.res.R
45 import com.android.systemui.settings.UserTracker
46 import javax.inject.Inject
47 import kotlinx.coroutines.CoroutineDispatcher
48 import kotlinx.coroutines.CoroutineScope
49 import kotlinx.coroutines.flow.MutableStateFlow
50 import kotlinx.coroutines.flow.SharingStarted
51 import kotlinx.coroutines.flow.combine
52 import kotlinx.coroutines.flow.distinctUntilChanged
53 import kotlinx.coroutines.flow.flowOn
54 import kotlinx.coroutines.flow.map
55 import kotlinx.coroutines.flow.stateIn
56 import kotlinx.coroutines.withContext
57 
58 class ShortcutHelperViewModel
59 @Inject
60 constructor(
61     private val context: Context,
62     private val roleManager: RoleManager,
63     private val userTracker: UserTracker,
64     @Background private val backgroundScope: CoroutineScope,
65     @Background private val backgroundDispatcher: CoroutineDispatcher,
66     private val stateInteractor: ShortcutHelperStateInteractor,
67     categoriesInteractor: ShortcutHelperCategoriesInteractor,
68     private val customizationModeInteractor: ShortcutHelperCustomizationModeInteractor,
69 ) {
70 
71     private val searchQuery = MutableStateFlow("")
72     private val userContext = userTracker.createCurrentUserContext(userTracker.userContext)
73 
74     val shouldShow =
75         categoriesInteractor.shortcutCategories
76             .map { it.isNotEmpty() }
77             .distinctUntilChanged()
78             .flowOn(backgroundDispatcher)
79 
80     val shortcutsUiState =
81         combine(
82                 searchQuery,
83                 categoriesInteractor.shortcutCategories,
84                 customizationModeInteractor.customizationMode,
85             ) { query, categories, isCustomizationModeEnabled ->
86                 if (categories.isEmpty()) {
87                     ShortcutsUiState.Inactive
88                 } else {
89                     /* temporarily hiding launcher shortcut categories until b/327141011
90                      * is completed. */
91                     val categoriesWithLauncherExcluded = excludeLauncherApp(categories)
92                     val filteredCategories =
93                         filterCategoriesBySearchQuery(query, categoriesWithLauncherExcluded)
94                     val shortcutCategoriesUi = convertCategoriesModelToUiModel(filteredCategories)
95                     ShortcutsUiState.Active(
96                         searchQuery = query,
97                         shortcutCategories = shortcutCategoriesUi,
98                         defaultSelectedCategory = getDefaultSelectedCategory(filteredCategories),
99                         isShortcutCustomizerFlagEnabled =
100                             keyboardShortcutHelperShortcutCustomizer(),
101                         shouldShowResetButton = shouldShowResetButton(shortcutCategoriesUi),
102                         isCustomizationModeEnabled = isCustomizationModeEnabled,
103                     )
104                 }
105             }
106             .stateIn(
107                 scope = backgroundScope,
108                 started = SharingStarted.Lazily,
109                 initialValue = ShortcutsUiState.Inactive,
110             )
111 
112     private fun shouldShowResetButton(categoriesUi: List<ShortcutCategoryUi>): Boolean {
113         return categoriesUi.any { it.containsCustomShortcuts }
114     }
115 
116     private fun convertCategoriesModelToUiModel(
117         categories: List<ShortcutCategory>
118     ): List<ShortcutCategoryUi> {
119         return categories.map { category ->
120             ShortcutCategoryUi(
121                 label = getShortcutCategoryLabel(category.type),
122                 iconSource = getShortcutCategoryIcon(category.type),
123                 shortcutCategory = category,
124             )
125         }
126     }
127 
128     private fun getShortcutCategoryIcon(type: ShortcutCategoryType): IconSource {
129         return when (type) {
130             ShortcutCategoryType.System -> IconSource(imageVector = Icons.Default.Tv)
131             ShortcutCategoryType.MultiTasking ->
132                 IconSource(imageVector = Icons.Default.VerticalSplit)
133             ShortcutCategoryType.InputMethodEditor ->
134                 IconSource(imageVector = Icons.Default.Keyboard)
135             ShortcutCategoryType.AppCategories -> IconSource(imageVector = Icons.Default.Apps)
136             is CurrentApp -> {
137                 try {
138                     val iconDrawable =
139                         userContext.packageManager.getApplicationIcon(type.packageName)
140                     IconSource(painter = DrawablePainter(drawable = iconDrawable))
141                 } catch (_: NameNotFoundException) {
142                     Log.w(
143                         "ShortcutHelperViewModel",
144                         "Package not found when retrieving icon for ${type.packageName}",
145                     )
146                     IconSource(imageVector = Icons.Default.Android)
147                 }
148             }
149 
150             ShortcutCategoryType.Accessibility ->
151                 IconSource(imageVector = Icons.Default.AccessibilityNew)
152         }
153     }
154 
155     private fun getShortcutCategoryLabel(type: ShortcutCategoryType): String =
156         when (type) {
157             ShortcutCategoryType.System ->
158                 context.getString(R.string.shortcut_helper_category_system)
159             ShortcutCategoryType.MultiTasking ->
160                 context.getString(R.string.shortcut_helper_category_multitasking)
161             ShortcutCategoryType.InputMethodEditor ->
162                 context.getString(R.string.shortcut_helper_category_input)
163             ShortcutCategoryType.AppCategories ->
164                 context.getString(R.string.shortcut_helper_category_app_shortcuts)
165             is CurrentApp -> getApplicationLabelForCurrentApp(type)
166             ShortcutCategoryType.Accessibility ->
167                 context.getString(R.string.shortcutHelper_category_accessibility)
168         }
169 
170     private fun getApplicationLabelForCurrentApp(type: CurrentApp): String {
171         try {
172             val packageManagerForUser = userContext.packageManager
173             val currentAppInfo =
174                 packageManagerForUser.getApplicationInfo(type.packageName, /* flags= */ 0)
175             return packageManagerForUser.getApplicationLabel(currentAppInfo).toString()
176         } catch (e: NameNotFoundException) {
177             Log.w(
178                 "ShortcutHelperViewModel",
179                 "Package Not found when retrieving Label for ${type.packageName}",
180             )
181             return "Current App"
182         }
183     }
184 
185     private suspend fun excludeLauncherApp(
186         categories: List<ShortcutCategory>
187     ): List<ShortcutCategory> {
188         val launcherAppCategory =
189             categories.firstOrNull { it.type is CurrentApp && isLauncherApp(it.type.packageName) }
190         return if (launcherAppCategory != null) {
191             categories - launcherAppCategory
192         } else {
193             categories
194         }
195     }
196 
197     private suspend fun getDefaultSelectedCategory(
198         categories: List<ShortcutCategory>
199     ): ShortcutCategoryType? {
200         val currentAppShortcuts =
201             categories.firstOrNull { it.type is CurrentApp && !isLauncherApp(it.type.packageName) }
202         return currentAppShortcuts?.type ?: categories.firstOrNull()?.type
203     }
204 
205     private suspend fun isLauncherApp(packageName: String): Boolean {
206         return withContext(backgroundDispatcher) {
207             roleManager
208                 .getRoleHoldersAsUser(RoleManager.ROLE_HOME, userTracker.userHandle)
209                 .firstOrNull() == packageName
210         }
211     }
212 
213     private fun filterCategoriesBySearchQuery(
214         query: String,
215         categories: List<ShortcutCategory>,
216     ): List<ShortcutCategory> {
217         val lowerCaseTrimmedQuery = query.trim().lowercase()
218         if (lowerCaseTrimmedQuery.isEmpty()) {
219             return categories
220         }
221         return categories
222             .map { category ->
223                 category.copy(
224                     subCategories =
225                         filterSubCategoriesBySearchQuery(
226                             subCategories = category.subCategories,
227                             query = lowerCaseTrimmedQuery,
228                         )
229                 )
230             }
231             .filter { it.subCategories.isNotEmpty() }
232     }
233 
234     private fun filterSubCategoriesBySearchQuery(
235         subCategories: List<ShortcutSubCategory>,
236         query: String,
237     ) =
238         subCategories
239             .map { subCategory ->
240                 subCategory.copy(
241                     shortcuts = filterShortcutsBySearchQuery(subCategory.shortcuts, query)
242                 )
243             }
244             .filter { it.shortcuts.isNotEmpty() }
245 
246     private fun filterShortcutsBySearchQuery(shortcuts: List<Shortcut>, query: String) =
247         shortcuts.filter { shortcut -> shortcut.label.trim().lowercase().contains(query) }
248 
249     fun onViewClosed() {
250         stateInteractor.onViewClosed()
251         resetSearchQuery()
252         resetCustomizationMode()
253     }
254 
255     fun onViewOpened() {
256         stateInteractor.onViewOpened()
257     }
258 
259     fun onSearchQueryChanged(query: String) {
260         searchQuery.value = query
261     }
262 
263     fun toggleCustomizationMode(isCustomizing: Boolean) {
264         customizationModeInteractor.toggleCustomizationMode(isCustomizing)
265     }
266 
267     private fun resetSearchQuery() {
268         searchQuery.value = ""
269     }
270 
271     private fun resetCustomizationMode() {
272         customizationModeInteractor.toggleCustomizationMode(false)
273     }
274 }
275