1 /* 2 * 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.AppWidgetProviderInfo 20 import android.content.ComponentName 21 import android.os.UserHandle 22 import com.android.compose.animation.scene.ObservableTransitionState 23 import com.android.compose.animation.scene.SceneKey 24 import com.android.compose.animation.scene.TransitionKey 25 import com.android.systemui.communal.domain.interactor.CommunalInteractor 26 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor 27 import com.android.systemui.communal.domain.model.CommunalContentModel 28 import com.android.systemui.communal.shared.model.EditModeState 29 import com.android.systemui.communal.widgets.WidgetConfigurator 30 import com.android.systemui.keyguard.shared.model.KeyguardState 31 import com.android.systemui.media.controls.ui.controller.MediaCarouselController 32 import com.android.systemui.media.controls.ui.view.MediaHost 33 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf 34 import com.android.systemui.util.kotlin.BooleanFlowOperators.not 35 import kotlinx.coroutines.flow.Flow 36 import kotlinx.coroutines.flow.MutableStateFlow 37 import kotlinx.coroutines.flow.StateFlow 38 import kotlinx.coroutines.flow.asStateFlow 39 import kotlinx.coroutines.flow.flowOf 40 41 /** The base view model for the communal hub. */ 42 abstract class BaseCommunalViewModel( 43 val communalSceneInteractor: CommunalSceneInteractor, 44 private val communalInteractor: CommunalInteractor, 45 val mediaHost: MediaHost, 46 val mediaCarouselController: MediaCarouselController, 47 ) { 48 val currentScene: StateFlow<SceneKey> = communalSceneInteractor.currentScene 49 50 /** Used to animate showing or hiding the communal content. */ 51 open val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(false) 52 53 /** Whether communal hub should be focused by accessibility tools. */ 54 open val isFocusable: Flow<Boolean> = MutableStateFlow(false) 55 56 /** Whether widgets are currently being re-ordered. */ 57 open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) 58 59 /** The key of the currently selected item, or null if no item selected. */ 60 val selectedKey: StateFlow<String?> = communalInteractor.selectedKey 61 62 private val _isTouchConsumed: MutableStateFlow<Boolean> = MutableStateFlow(false) 63 64 /** Whether an element inside the lazy grid is actively consuming touches */ 65 val isTouchConsumed: Flow<Boolean> = _isTouchConsumed.asStateFlow() 66 67 private val _isNestedScrolling: MutableStateFlow<Boolean> = MutableStateFlow(false) 68 69 /** Whether the lazy grid is reporting scrolling within itself */ 70 val isNestedScrolling: Flow<Boolean> = _isNestedScrolling.asStateFlow() 71 72 /** 73 * Whether touch is available to be consumed by a touch handler. Touch is available during 74 * nested scrolling as lazy grid reports this for all scroll directions that it detects. In the 75 * case that there is consumed scrolling on a nested element, such as an AndroidView, no nested 76 * scrolling will be reported. It is up to the flow consumer to determine whether the nested 77 * scroll can be applied. In the communal case, this would be identifying the scroll as 78 * vertical, which the lazy horizontal grid does not handle. 79 */ 80 val glanceableTouchAvailable: Flow<Boolean> = anyOf(not(isTouchConsumed), isNestedScrolling) 81 82 /** 83 * The up-to-date value of the grid scroll offset. persisted to interactor on 84 * {@link #persistScrollPosition} 85 */ 86 private var currentScrollOffset = 0 87 88 /** 89 * The up-to-date value of the grid scroll index. persisted to interactor on 90 * {@link #persistScrollPosition} 91 */ 92 private var currentScrollIndex = 0 93 signalUserInteractionnull94 fun signalUserInteraction() { 95 communalInteractor.signalUserInteraction() 96 } 97 98 /** 99 * Asks for an asynchronous scene witch to [newScene], which will use the corresponding 100 * installed transition or the one specified by [transitionKey], if provided. 101 */ changeScenenull102 fun changeScene( 103 scene: SceneKey, 104 loggingReason: String, 105 transitionKey: TransitionKey? = null, 106 keyguardState: KeyguardState? = null, 107 ) { 108 communalSceneInteractor.changeScene(scene, loggingReason, transitionKey, keyguardState) 109 } 110 setEditModeStatenull111 fun setEditModeState(state: EditModeState?) = communalSceneInteractor.setEditModeState(state) 112 113 /** 114 * Updates the transition state of the hub [SceneTransitionLayout]. 115 * 116 * Note that you must call is with `null` when the UI is done or risk a memory leak. 117 */ 118 fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { 119 communalSceneInteractor.setTransitionState(transitionState) 120 } 121 onOpenEnableWidgetDialognull122 open fun onOpenEnableWidgetDialog() {} 123 onOpenEnableWorkProfileDialognull124 open fun onOpenEnableWorkProfileDialog() {} 125 126 /** A list of all the communal content to be displayed in the communal hub. */ 127 abstract val communalContent: Flow<List<CommunalContentModel>> 128 129 /** 130 * Whether to freeze the emission of the communalContent flow to prevent recomposition. Defaults 131 * to false, indicating that the flow will emit new update. 132 */ 133 open val isCommunalContentFlowFrozen: Flow<Boolean> = flowOf(false) 134 135 /** Whether in edit mode for the communal hub. */ 136 open val isEditMode = false 137 138 /** Whether the type of popup currently showing */ 139 open val currentPopup: Flow<PopupType?> = flowOf(null) 140 141 /** Whether the communal hub is empty with no widget available. */ 142 open val isEmptyState: Flow<Boolean> = flowOf(false) 143 144 /** Called as the UI request to dismiss the any displaying popup */ onHidePopupnull145 open fun onHidePopup() {} 146 147 /** Called as the UI requests adding a widget. */ onAddWidgetnull148 open fun onAddWidget( 149 componentName: ComponentName, 150 user: UserHandle, 151 rank: Int? = null, 152 configurator: WidgetConfigurator? = null, 153 ) {} 154 155 /** Called as the UI requests deleting a widget. */ onDeleteWidgetnull156 open fun onDeleteWidget(id: Int, key: String, componentName: ComponentName, rank: Int) {} 157 158 /** Called as the UI detects a tap event on the widget. */ onTapWidgetnull159 open fun onTapWidget(componentName: ComponentName, rank: Int) {} 160 161 /** 162 * Called as the UI requests reordering widgets. 163 * 164 * @param widgetIdToRankMap mapping of the widget ids to its rank. When re-ordering to add a new 165 * item in the middle, provide the priorities of existing widgets as if the new item existed, 166 * and then, call [onAddWidget] to add the new item at intended order. 167 */ onReorderWidgetsnull168 open fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) {} 169 170 /** 171 * Called as the UI requests resizing a widget. 172 * 173 * @param appWidgetId The id of the widget being resized. 174 * @param spanY The new size of the widget, in grid spans. 175 * @param widgetIdToRankMap Mapping of the widget ids to its rank. Allows re-ordering widgets 176 * alongside the resize, in case resizing also requires re-ordering. This ensures the 177 * re-ordering is done in the same database transaction as the resize. 178 */ onResizeWidgetnull179 open fun onResizeWidget( 180 appWidgetId: Int, 181 spanY: Int, 182 widgetIdToRankMap: Map<Int, Int>, 183 componentName: ComponentName, 184 rank: Int, 185 ) {} 186 187 /** Called as the UI requests opening the widget editor with an optional preselected widget. */ onOpenWidgetEditornull188 open fun onOpenWidgetEditor(shouldOpenWidgetPickerOnStart: Boolean = false) {} 189 190 /** Called as the UI requests to dismiss the CTA tile. */ onDismissCtaTilenull191 open fun onDismissCtaTile() {} 192 193 /** Called as the user starts dragging a widget to reorder. */ onReorderWidgetStartnull194 open fun onReorderWidgetStart(draggingItemKey: String) {} 195 196 /** Called as the user finishes dragging a widget to reorder. */ onReorderWidgetEndnull197 open fun onReorderWidgetEnd() {} 198 199 /** Called as the user cancels dragging a widget to reorder. */ onReorderWidgetCancelnull200 open fun onReorderWidgetCancel() {} 201 202 /** Called as the user request to show the customize widget button. */ onLongClicknull203 open fun onLongClick() {} 204 205 /** Called as the user requests to switch to the previous player in UMO. */ onShowPreviousMedianull206 open fun onShowPreviousMedia() {} 207 208 /** Called as the user requests to switch to the next player in UMO. */ onShowNextMedianull209 open fun onShowNextMedia() {} 210 211 /** Called as the UI determines that a new widget has been added to the grid. */ onNewWidgetAddednull212 open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {} 213 214 /** Called when the grid scroll position has been updated. */ onScrollPositionUpdatednull215 open fun onScrollPositionUpdated(firstVisibleItemIndex: Int, firstVisibleItemScroll: Int) { 216 currentScrollIndex = firstVisibleItemIndex 217 currentScrollOffset = firstVisibleItemScroll 218 } 219 220 /** Stores scroll values to interactor. */ persistScrollPositionnull221 protected fun persistScrollPosition() { 222 communalInteractor.setScrollPosition(currentScrollIndex, currentScrollOffset) 223 } 224 225 /** Invoked after scroll values are used to initialize grid position. */ clearPersistedScrollPositionnull226 open fun clearPersistedScrollPosition() { 227 communalInteractor.setScrollPosition(0, 0) 228 } 229 230 val savedFirstScrollIndex: Int 231 get() = communalInteractor.firstVisibleItemIndex 232 233 val savedFirstScrollOffset: Int 234 get() = communalInteractor.firstVisibleItemOffset 235 236 /** Set the key of the currently selected item */ setSelectedKeynull237 fun setSelectedKey(key: String?) { 238 communalInteractor.setSelectedKey(key) 239 } 240 241 /** Invoked once touches inside the lazy grid are consumed */ onHubTouchConsumednull242 fun onHubTouchConsumed() { 243 if (_isTouchConsumed.value) { 244 return 245 } 246 247 _isTouchConsumed.value = true 248 } 249 250 /** Invoked when nested scrolling begins on the lazy grid */ onNestedScrollingnull251 fun onNestedScrolling() { 252 if (_isNestedScrolling.value) { 253 return 254 } 255 256 _isNestedScrolling.value = true 257 } 258 259 /** Resets nested scroll and touch consumption state */ onResetTouchStatenull260 fun onResetTouchState() { 261 _isTouchConsumed.value = false 262 _isNestedScrolling.value = false 263 } 264 } 265