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