• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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