1 /* <lambda>null2 * Copyright (C) 2023 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.communal.ui.viewmodel 18 19 import android.appwidget.AppWidgetManager 20 import android.appwidget.AppWidgetProviderInfo 21 import android.content.ComponentName 22 import android.content.Context 23 import android.content.Intent 24 import android.content.pm.PackageManager 25 import android.content.res.Resources 26 import android.os.UserHandle 27 import android.util.Log 28 import android.view.accessibility.AccessibilityEvent 29 import android.view.accessibility.AccessibilityManager 30 import com.android.internal.logging.UiEventLogger 31 import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE 32 import com.android.systemui.communal.data.model.CommunalWidgetCategories 33 import com.android.systemui.communal.domain.interactor.CommunalInteractor 34 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 35 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor 36 import com.android.systemui.communal.domain.model.CommunalContentModel 37 import com.android.systemui.communal.shared.log.CommunalMetricsLogger 38 import com.android.systemui.communal.shared.log.CommunalUiEvent 39 import com.android.systemui.communal.shared.model.EditModeState 40 import com.android.systemui.communal.widgets.WidgetConfigurator 41 import com.android.systemui.dagger.SysUISingleton 42 import com.android.systemui.dagger.qualifiers.Application 43 import com.android.systemui.dagger.qualifiers.Background 44 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 45 import com.android.systemui.keyguard.shared.model.KeyguardState 46 import com.android.systemui.log.LogBuffer 47 import com.android.systemui.log.core.Logger 48 import com.android.systemui.log.dagger.CommunalLog 49 import com.android.systemui.media.controls.ui.controller.MediaCarouselController 50 import com.android.systemui.media.controls.ui.view.MediaHost 51 import com.android.systemui.media.dagger.MediaModule 52 import com.android.systemui.res.R 53 import com.android.systemui.scene.shared.model.Scenes 54 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf 55 import com.android.systemui.util.kotlin.BooleanFlowOperators.not 56 import javax.inject.Inject 57 import javax.inject.Named 58 import kotlinx.coroutines.CoroutineDispatcher 59 import kotlinx.coroutines.flow.Flow 60 import kotlinx.coroutines.flow.MutableStateFlow 61 import kotlinx.coroutines.flow.StateFlow 62 import kotlinx.coroutines.flow.filter 63 import kotlinx.coroutines.flow.first 64 import kotlinx.coroutines.flow.map 65 import kotlinx.coroutines.flow.onEach 66 import kotlinx.coroutines.withContext 67 68 /** The view model for communal hub in edit mode. */ 69 @SysUISingleton 70 class CommunalEditModeViewModel 71 @Inject 72 constructor( 73 communalSceneInteractor: CommunalSceneInteractor, 74 private val communalInteractor: CommunalInteractor, 75 private val communalSettingsInteractor: CommunalSettingsInteractor, 76 keyguardTransitionInteractor: KeyguardTransitionInteractor, 77 @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, 78 private val uiEventLogger: UiEventLogger, 79 @CommunalLog logBuffer: LogBuffer, 80 @Background private val backgroundDispatcher: CoroutineDispatcher, 81 private val metricsLogger: CommunalMetricsLogger, 82 @Application private val context: Context, 83 private val accessibilityManager: AccessibilityManager, 84 private val packageManager: PackageManager, 85 @Named(LAUNCHER_PACKAGE) private val launcherPackage: String, 86 mediaCarouselController: MediaCarouselController, 87 ) : 88 BaseCommunalViewModel( 89 communalSceneInteractor, 90 communalInteractor, 91 mediaHost, 92 mediaCarouselController, 93 ) { 94 95 private val logger = Logger(logBuffer, "CommunalEditModeViewModel") 96 97 override val isEditMode = true 98 99 override val isCommunalContentVisible: Flow<Boolean> = 100 communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING } 101 102 val showDisclaimer: Flow<Boolean> = 103 allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed)) 104 105 fun onDisclaimerDismissed() { 106 communalInteractor.setDisclaimerDismissed() 107 } 108 109 /** 110 * Emits when edit mode activity can show, after we've transitioned to [KeyguardState.GONE] and 111 * edit mode is open. 112 */ 113 val canShowEditMode = 114 allOf( 115 keyguardTransitionInteractor.isFinishedIn( 116 content = Scenes.Gone, 117 stateWithoutSceneContainer = KeyguardState.GONE, 118 ), 119 communalInteractor.editModeOpen, 120 ) 121 .filter { it } 122 123 // Only widgets are editable. 124 override val communalContent: Flow<List<CommunalContentModel>> = 125 communalInteractor.widgetContent.onEach { models -> 126 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } } 127 } 128 129 private val _reorderingWidgets = MutableStateFlow(false) 130 131 override val reorderingWidgets: StateFlow<Boolean> 132 get() = _reorderingWidgets 133 134 override fun onAddWidget( 135 componentName: ComponentName, 136 user: UserHandle, 137 rank: Int?, 138 configurator: WidgetConfigurator?, 139 ) { 140 communalInteractor.addWidget(componentName, user, rank, configurator) 141 metricsLogger.logAddWidget(componentName.flattenToString(), rank) 142 } 143 144 override fun onDeleteWidget(id: Int, key: String, componentName: ComponentName, rank: Int) { 145 if (selectedKey.value == key) { 146 setSelectedKey(null) 147 } 148 communalInteractor.deleteWidget(id) 149 metricsLogger.logRemoveWidget(componentName.flattenToString(), rank) 150 } 151 152 override fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) = 153 communalInteractor.updateWidgetOrder(widgetIdToRankMap) 154 155 override fun onResizeWidget( 156 appWidgetId: Int, 157 spanY: Int, 158 widgetIdToRankMap: Map<Int, Int>, 159 componentName: ComponentName, 160 rank: Int, 161 ) { 162 communalInteractor.resizeWidget(appWidgetId, spanY, widgetIdToRankMap) 163 metricsLogger.logResizeWidget( 164 componentName = componentName.flattenToString(), 165 rank = rank, 166 spanY = spanY, 167 ) 168 } 169 170 override fun onReorderWidgetStart(draggingItemKey: String) { 171 setSelectedKey(draggingItemKey) 172 _reorderingWidgets.value = true 173 uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) 174 } 175 176 override fun onReorderWidgetEnd() { 177 _reorderingWidgets.value = false 178 uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) 179 } 180 181 override fun onReorderWidgetCancel() { 182 _reorderingWidgets.value = false 183 uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) 184 } 185 186 override fun onNewWidgetAdded(provider: AppWidgetProviderInfo) { 187 if (!accessibilityManager.isEnabled) { 188 return 189 } 190 191 // Send an accessibility announcement for the newly added widget 192 val widgetLabel = provider.loadLabel(packageManager) 193 val announcementText = 194 context.getString( 195 R.string.accessibility_announcement_communal_widget_added, 196 widgetLabel, 197 ) 198 accessibilityManager.sendAccessibilityEvent( 199 AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply { 200 contentDescription = announcementText 201 } 202 ) 203 } 204 205 val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal 206 207 /** Launch the widget picker activity using the given startActivity method. */ 208 suspend fun onOpenWidgetPicker( 209 resources: Resources, 210 startActivity: (intent: Intent) -> Unit, 211 ): Boolean = 212 withContext(backgroundDispatcher) { 213 val widgets = communalInteractor.widgetContent.first() 214 val excludeList = 215 widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo( 216 ArrayList() 217 ) { 218 it.providerInfo 219 } 220 getWidgetPickerActivityIntent(resources, excludeList)?.let { 221 try { 222 startActivity(it) 223 return@withContext true 224 } catch (e: Exception) { 225 Log.e(TAG, "Failed to launch widget picker activity", e) 226 } 227 } 228 false 229 } 230 231 private fun getWidgetPickerActivityIntent( 232 resources: Resources, 233 excludeList: ArrayList<AppWidgetProviderInfo>, 234 ): Intent? { 235 return Intent(Intent.ACTION_PICK).apply { 236 setPackage(launcherPackage) 237 putExtra( 238 EXTRA_DESIRED_WIDGET_WIDTH, 239 resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width), 240 ) 241 putExtra( 242 EXTRA_DESIRED_WIDGET_HEIGHT, 243 resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height), 244 ) 245 putExtra( 246 AppWidgetManager.EXTRA_CATEGORY_FILTER, 247 CommunalWidgetCategories.includedCategories, 248 ) 249 putExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, CommunalWidgetCategories.excludedCategories) 250 251 communalSettingsInteractor.workProfileUserDisallowedByDevicePolicy.value?.let { 252 putExtra(EXTRA_USER_ID_FILTER, arrayListOf(it.id)) 253 } 254 putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE) 255 putExtra(EXTRA_PICKER_TITLE, resources.getString(R.string.communal_widget_picker_title)) 256 putExtra( 257 EXTRA_PICKER_DESCRIPTION, 258 resources.getString(R.string.communal_widget_picker_description), 259 ) 260 putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList) 261 } 262 } 263 264 /** Sets whether edit mode is currently open */ 265 fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) 266 267 /** 268 * Sets whether the edit mode activity is currently showing. 269 * 270 * See [CommunalInteractor.editActivityShowing] for more info. 271 */ 272 fun setEditActivityShowing(showing: Boolean) = 273 communalInteractor.setEditActivityShowing(showing) 274 275 /** Called when exiting the edit mode, before transitioning back to the communal scene. */ 276 fun cleanupEditModeState() { 277 communalSceneInteractor.setEditModeState(null) 278 279 // Set the scroll position of the glanceable hub to match where we are now. 280 persistScrollPosition() 281 } 282 283 companion object { 284 private const val TAG = "CommunalEditModeViewModel" 285 286 private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" 287 private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" 288 private const val EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter" 289 private const val EXTRA_PICKER_TITLE = "picker_title" 290 private const val EXTRA_PICKER_DESCRIPTION = "picker_description" 291 private const val EXTRA_UI_SURFACE_KEY = "ui_surface" 292 private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub" 293 private const val EXTRA_USER_ID_FILTER = "filtered_user_ids" 294 const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets" 295 } 296 } 297