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.content.res.Resources
20 import android.view.View
21 import android.view.accessibility.AccessibilityNodeInfo
22 import com.android.systemui.communal.domain.interactor.CommunalInteractor
23 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
24 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
25 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
26 import com.android.systemui.communal.domain.model.CommunalContentModel
27 import com.android.systemui.communal.shared.model.CommunalBackgroundType
28 import com.android.systemui.dagger.SysUISingleton
29 import com.android.systemui.dagger.qualifiers.Application
30 import com.android.systemui.dagger.qualifiers.Main
31 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
32 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
33 import com.android.systemui.keyguard.shared.model.KeyguardState
34 import com.android.systemui.log.LogBuffer
35 import com.android.systemui.log.core.Logger
36 import com.android.systemui.log.dagger.CommunalLog
37 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
38 import com.android.systemui.media.controls.ui.view.MediaHost
39 import com.android.systemui.media.controls.ui.view.MediaHostState
40 import com.android.systemui.media.dagger.MediaModule
41 import com.android.systemui.res.R
42 import com.android.systemui.shade.domain.interactor.ShadeInteractor
43 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
44 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
45 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
46 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
47 import javax.inject.Inject
48 import javax.inject.Named
49 import kotlinx.coroutines.CoroutineDispatcher
50 import kotlinx.coroutines.CoroutineScope
51 import kotlinx.coroutines.ExperimentalCoroutinesApi
52 import kotlinx.coroutines.Job
53 import kotlinx.coroutines.channels.awaitClose
54 import kotlinx.coroutines.delay
55 import kotlinx.coroutines.flow.Flow
56 import kotlinx.coroutines.flow.MutableStateFlow
57 import kotlinx.coroutines.flow.asStateFlow
58 import kotlinx.coroutines.flow.combine
59 import kotlinx.coroutines.flow.distinctUntilChanged
60 import kotlinx.coroutines.flow.flatMapLatest
61 import kotlinx.coroutines.flow.flowOf
62 import kotlinx.coroutines.flow.flowOn
63 import kotlinx.coroutines.flow.map
64 import kotlinx.coroutines.flow.onEach
65 import kotlinx.coroutines.flow.onStart
66 import kotlinx.coroutines.launch
67 
68 /** The default view model used for showing the communal hub. */
69 @OptIn(ExperimentalCoroutinesApi::class)
70 @SysUISingleton
71 class CommunalViewModel
72 @Inject
73 constructor(
74     @Main val mainDispatcher: CoroutineDispatcher,
75     @Application private val scope: CoroutineScope,
76     @Main private val resources: Resources,
77     keyguardTransitionInteractor: KeyguardTransitionInteractor,
78     keyguardInteractor: KeyguardInteractor,
79     communalSceneInteractor: CommunalSceneInteractor,
80     private val communalInteractor: CommunalInteractor,
81     private val communalSettingsInteractor: CommunalSettingsInteractor,
82     tutorialInteractor: CommunalTutorialInteractor,
83     private val shadeInteractor: ShadeInteractor,
84     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
85     @CommunalLog logBuffer: LogBuffer,
86 ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) {
87 
88     private val _isMediaHostVisible =
89         conflatedCallbackFlow<Boolean> {
90                 val callback = { visible: Boolean ->
91                     trySend(visible)
92                     Unit
93                 }
94                 mediaHost.addVisibilityChangeListener(callback)
95                 awaitClose { mediaHost.removeVisibilityChangeListener(callback) }
96             }
97             .onStart { emit(mediaHost.visible) }
98             .flowOn(mainDispatcher)
99 
100     private val logger = Logger(logBuffer, "CommunalViewModel")
101 
102     /** Communal content saved from the previous emission when the flow is active (not "frozen"). */
103     private var frozenCommunalContent: List<CommunalContentModel>? = null
104 
105     @OptIn(ExperimentalCoroutinesApi::class)
106     private val latestCommunalContent: Flow<List<CommunalContentModel>> =
107         tutorialInteractor.isTutorialAvailable
108             .flatMapLatest { isTutorialMode ->
109                 if (isTutorialMode) {
110                     return@flatMapLatest flowOf(communalInteractor.tutorialContent)
111                 }
112                 val ongoingContent =
113                     _isMediaHostVisible.flatMapLatest { communalInteractor.getOngoingContent(it) }
114                 combine(
115                     ongoingContent,
116                     communalInteractor.widgetContent,
117                     communalInteractor.ctaTileContent,
118                 ) { ongoing, widgets, ctaTile,
119                     ->
120                     ongoing + widgets + ctaTile
121                 }
122             }
123             .onEach { models ->
124                 frozenCommunalContent = models
125                 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
126             }
127 
128     override val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(true)
129 
130     /**
131      * Freeze the content flow, when an activity is about to show, like starting a timer via voice:
132      * 1) in handheld mode, use the keyguard occluded state;
133      * 2) in dreaming mode, where keyguard is already occluded by dream, use the dream wakeup
134      *    signal. Since in this case the shell transition info does not include
135      *    KEYGUARD_VISIBILITY_TRANSIT_FLAGS, KeyguardTransitionHandler will not run the
136      *    occludeAnimation on KeyguardViewMediator.
137      */
138     override val isCommunalContentFlowFrozen: Flow<Boolean> =
139         allOf(
140                 keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB),
141                 keyguardInteractor.isKeyguardOccluded,
142                 not(keyguardInteractor.isAbleToDream)
143             )
144             .distinctUntilChanged()
145             .onEach { logger.d("isCommunalContentFlowFrozen: $it") }
146 
147     override val communalContent: Flow<List<CommunalContentModel>> =
148         isCommunalContentFlowFrozen
149             .flatMapLatestConflated { isFrozen ->
150                 if (isFrozen) {
151                     flowOf(frozenCommunalContent ?: emptyList())
152                 } else {
153                     latestCommunalContent
154                 }
155             }
156             .onEach { models ->
157                 logger.d({ "CommunalContent: $str1" }) { str1 = models.joinToString { it.key } }
158             }
159 
160     override val isEmptyState: Flow<Boolean> =
161         communalInteractor.widgetContent
162             .map { it.isEmpty() }
163             .distinctUntilChanged()
164             .onEach { logger.d("isEmptyState: $it") }
165 
166     private val _currentPopup: MutableStateFlow<PopupType?> = MutableStateFlow(null)
167     override val currentPopup: Flow<PopupType?> = _currentPopup.asStateFlow()
168 
169     // The widget is focusable for accessibility when the hub is fully visible and shade is not
170     // opened.
171     override val isFocusable: Flow<Boolean> =
172         combine(
173                 keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB),
174                 communalInteractor.isIdleOnCommunal,
175                 shadeInteractor.isAnyFullyExpanded,
176             ) { transitionedToGlanceableHub, isIdleOnCommunal, isAnyFullyExpanded ->
177                 transitionedToGlanceableHub && isIdleOnCommunal && !isAnyFullyExpanded
178             }
179             .distinctUntilChanged()
180 
181     override val widgetAccessibilityDelegate =
182         object : View.AccessibilityDelegate() {
183             override fun onInitializeAccessibilityNodeInfo(
184                 host: View,
185                 info: AccessibilityNodeInfo
186             ) {
187                 super.onInitializeAccessibilityNodeInfo(host, info)
188                 // Hint user to long press in order to enter edit mode
189                 info.addAction(
190                     AccessibilityNodeInfo.AccessibilityAction(
191                         AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
192                         resources
193                             .getString(R.string.accessibility_action_label_edit_widgets)
194                             .lowercase()
195                     )
196                 )
197             }
198         }
199 
200     private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
201     val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow()
202 
203     private val _isEnableWorkProfileDialogShowing: MutableStateFlow<Boolean> =
204         MutableStateFlow(false)
205     val isEnableWorkProfileDialogShowing: Flow<Boolean> =
206         _isEnableWorkProfileDialogShowing.asStateFlow()
207 
208     init {
209         // Initialize our media host for the UMO. This only needs to happen once and must be done
210         // before the MediaHierarchyManager attempts to move the UMO to the hub.
211         with(mediaHost) {
212             expansion = MediaHostState.EXPANDED
213             expandedMatchesParentHeight = true
214             showsOnlyActiveMedia = true
215             falsingProtectionNeeded = false
216             init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
217         }
218     }
219 
220     override fun onOpenWidgetEditor(
221         preselectedKey: String?,
222         shouldOpenWidgetPickerOnStart: Boolean,
223     ) = communalInteractor.showWidgetEditor(preselectedKey, shouldOpenWidgetPickerOnStart)
224 
225     override fun onDismissCtaTile() {
226         scope.launch {
227             communalInteractor.dismissCtaTile()
228             setCurrentPopupType(PopupType.CtaTile)
229         }
230     }
231 
232     override fun onShowCustomizeWidgetButton() {
233         setCurrentPopupType(PopupType.CustomizeWidgetButton)
234     }
235 
236     override fun onHidePopup() {
237         setCurrentPopupType(null)
238     }
239 
240     override fun onOpenEnableWidgetDialog() {
241         setIsEnableWidgetDialogShowing(true)
242     }
243 
244     fun onEnableWidgetDialogConfirm() {
245         communalInteractor.navigateToCommunalWidgetSettings()
246         setIsEnableWidgetDialogShowing(false)
247     }
248 
249     fun onEnableWidgetDialogCancel() {
250         setIsEnableWidgetDialogShowing(false)
251     }
252 
253     override fun onOpenEnableWorkProfileDialog() {
254         setIsEnableWorkProfileDialogShowing(true)
255     }
256 
257     fun onEnableWorkProfileDialogConfirm() {
258         communalInteractor.unpauseWorkProfile()
259         setIsEnableWorkProfileDialogShowing(false)
260     }
261 
262     fun onEnableWorkProfileDialogCancel() {
263         setIsEnableWorkProfileDialogShowing(false)
264     }
265 
266     private fun setIsEnableWidgetDialogShowing(isVisible: Boolean) {
267         _isEnableWidgetDialogShowing.value = isVisible
268     }
269 
270     private fun setIsEnableWorkProfileDialogShowing(isVisible: Boolean) {
271         _isEnableWorkProfileDialogShowing.value = isVisible
272     }
273 
274     private fun setCurrentPopupType(popupType: PopupType?) {
275         _currentPopup.value = popupType
276         delayedHideCurrentPopupJob?.cancel()
277 
278         if (popupType != null) {
279             delayedHideCurrentPopupJob =
280                 scope.launch {
281                     delay(POPUP_AUTO_HIDE_TIMEOUT_MS)
282                     setCurrentPopupType(null)
283                 }
284         } else {
285             delayedHideCurrentPopupJob = null
286         }
287     }
288 
289     private var delayedHideCurrentPopupJob: Job? = null
290 
291     /** Whether we can transition to a new scene based on a user gesture. */
292     fun canChangeScene(): Boolean {
293         return !shadeInteractor.isAnyFullyExpanded.value
294     }
295 
296     /**
297      * Whether touches should be disabled in communal.
298      *
299      * This is needed because the notification shade does not block touches in blank areas and these
300      * fall through to the glanceable hub, which we don't want.
301      */
302     val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
303 
304     // TODO(b/339667383): remove this temporary swipe gesture handle
305     /**
306      * The dream overlay has its own gesture handle as the SysUI window is not visible above the
307      * dream. This flow will be false when dreaming so that we don't show a duplicate handle when
308      * opening the hub over the dream.
309      */
310     val showGestureIndicator: Flow<Boolean> = not(keyguardInteractor.isDreaming)
311 
312     /** The type of background to use for the hub. */
313     val communalBackground: Flow<CommunalBackgroundType> =
314         communalSettingsInteractor.communalBackground
315 
316     companion object {
317         const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L
318     }
319 }
320 
321 sealed class PopupType {
322     object CtaTile : PopupType()
323 
324     object CustomizeWidgetButton : PopupType()
325 }
326