• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ComponentName
20 import com.android.app.tracing.coroutines.launchTraced as launch
21 import com.android.systemui.Flags
22 import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB
23 import com.android.systemui.communal.domain.interactor.CommunalInteractor
24 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
25 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
26 import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
27 import com.android.systemui.communal.domain.model.CommunalContentModel
28 import com.android.systemui.communal.shared.log.CommunalMetricsLogger
29 import com.android.systemui.communal.shared.model.CommunalBackgroundType
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.dagger.qualifiers.Background
33 import com.android.systemui.dagger.qualifiers.Main
34 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
35 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
36 import com.android.systemui.keyguard.shared.model.KeyguardState
37 import com.android.systemui.keyguard.ui.transitions.BlurConfig
38 import com.android.systemui.log.LogBuffer
39 import com.android.systemui.log.core.Logger
40 import com.android.systemui.log.dagger.CommunalLog
41 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
42 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
43 import com.android.systemui.media.controls.ui.view.MediaHost
44 import com.android.systemui.media.controls.ui.view.MediaHostState
45 import com.android.systemui.media.dagger.MediaModule
46 import com.android.systemui.scene.shared.model.Scenes
47 import com.android.systemui.shade.domain.interactor.ShadeInteractor
48 import com.android.systemui.statusbar.KeyguardIndicationController
49 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
50 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
51 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
52 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
53 import javax.inject.Inject
54 import javax.inject.Named
55 import kotlinx.coroutines.CoroutineDispatcher
56 import kotlinx.coroutines.CoroutineScope
57 import kotlinx.coroutines.Job
58 import kotlinx.coroutines.channels.awaitClose
59 import kotlinx.coroutines.delay
60 import kotlinx.coroutines.flow.Flow
61 import kotlinx.coroutines.flow.MutableStateFlow
62 import kotlinx.coroutines.flow.SharingStarted
63 import kotlinx.coroutines.flow.StateFlow
64 import kotlinx.coroutines.flow.asStateFlow
65 import kotlinx.coroutines.flow.combine
66 import kotlinx.coroutines.flow.distinctUntilChanged
67 import kotlinx.coroutines.flow.flatMapLatest
68 import kotlinx.coroutines.flow.flowOf
69 import kotlinx.coroutines.flow.flowOn
70 import kotlinx.coroutines.flow.map
71 import kotlinx.coroutines.flow.onEach
72 import kotlinx.coroutines.flow.onStart
73 import kotlinx.coroutines.flow.stateIn
74 
75 /** The default view model used for showing the communal hub. */
76 @SysUISingleton
77 class CommunalViewModel
78 @Inject
79 constructor(
80     @Main val mainDispatcher: CoroutineDispatcher,
81     @Application private val scope: CoroutineScope,
82     @Background private val bgScope: CoroutineScope,
83     keyguardTransitionInteractor: KeyguardTransitionInteractor,
84     keyguardInteractor: KeyguardInteractor,
85     private val keyguardIndicationController: KeyguardIndicationController,
86     communalSceneInteractor: CommunalSceneInteractor,
87     private val communalInteractor: CommunalInteractor,
88     private val communalSettingsInteractor: CommunalSettingsInteractor,
89     tutorialInteractor: CommunalTutorialInteractor,
90     private val shadeInteractor: ShadeInteractor,
91     @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
92     @CommunalLog logBuffer: LogBuffer,
93     private val metricsLogger: CommunalMetricsLogger,
94     mediaCarouselController: MediaCarouselController,
95     blurConfig: BlurConfig,
96     @Named(SWIPE_TO_HUB) private val swipeToHub: Boolean,
97 ) :
98     BaseCommunalViewModel(
99         communalSceneInteractor,
100         communalInteractor,
101         mediaHost,
102         mediaCarouselController,
103     ) {
104 
105     private val logger = Logger(logBuffer, "CommunalViewModel")
106 
107     private val isMediaHostVisible =
108         conflatedCallbackFlow {
109                 val callback = { visible: Boolean ->
110                     trySend(visible)
111                     Unit
112                 }
113                 mediaHost.addVisibilityChangeListener(callback)
114                 awaitClose { mediaHost.removeVisibilityChangeListener(callback) }
115             }
116             .onStart {
117                 // Ensure the visibility state is correct when the hub is opened and this flow is
118                 // started so that the UMO is shown when needed. The visibility state in MediaHost
119                 // is not updated once its view has been detached, aka the hub is closed, which can
120                 // result in this getting stuck as False and never being updated as the UMO is not
121                 // shown.
122                 mediaHost.updateViewVisibility()
123                 emit(mediaHost.visible)
124             }
125             .distinctUntilChanged()
126             .onEach { logger.d({ "_isMediaHostVisible: $bool1" }) { bool1 = it } }
127             .flowOn(mainDispatcher)
128 
129     /** Communal content saved from the previous emission when the flow is active (not "frozen"). */
130     private var frozenCommunalContent: List<CommunalContentModel>? = null
131 
132     private val ongoingContent =
133         isMediaHostVisible.flatMapLatest { isMediaHostVisible ->
134             communalInteractor.ongoingContent(isMediaHostVisible).onEach {
135                 mediaHost.updateViewVisibility()
136             }
137         }
138 
139     private val latestCommunalContent: Flow<List<CommunalContentModel>> =
140         tutorialInteractor.isTutorialAvailable
141             .flatMapLatest { isTutorialMode ->
142                 if (isTutorialMode) {
143                     return@flatMapLatest flowOf(communalInteractor.tutorialContent)
144                 }
145                 combine(
146                     ongoingContent,
147                     communalInteractor.widgetContent,
148                     communalInteractor.ctaTileContent,
149                 ) { ongoing, widgets, ctaTile ->
150                     ongoing + widgets + ctaTile
151                 }
152             }
153             .onEach { models ->
154                 frozenCommunalContent = models
155                 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
156             }
157 
158     override val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(true)
159 
160     /**
161      * Freeze the content flow, when an activity is about to show, like starting a timer via voice:
162      * 1) in handheld mode, use the keyguard occluded state;
163      * 2) in dreaming mode, where keyguard is already occluded by dream, use the dream wakeup
164      *    signal. Since in this case the shell transition info does not include
165      *    KEYGUARD_VISIBILITY_TRANSIT_FLAGS, KeyguardTransitionHandler will not run the
166      *    occludeAnimation on KeyguardViewMediator.
167      */
168     override val isCommunalContentFlowFrozen: Flow<Boolean> =
169         allOf(
170                 keyguardTransitionInteractor.isFinishedIn(
171                     content = Scenes.Communal,
172                     stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB,
173                 ),
174                 keyguardInteractor.isKeyguardOccluded,
175                 not(keyguardInteractor.isAbleToDream),
176             )
177             .distinctUntilChanged()
178             .onEach { logger.d("isCommunalContentFlowFrozen: $it") }
179 
180     override val communalContent: Flow<List<CommunalContentModel>> =
181         isCommunalContentFlowFrozen
182             .flatMapLatestConflated { isFrozen ->
183                 if (isFrozen) {
184                     flowOf(frozenCommunalContent ?: emptyList())
185                 } else {
186                     latestCommunalContent
187                 }
188             }
189             .onEach { models ->
190                 logger.d({ "CommunalContent: $str1" }) { str1 = models.joinToString { it.key } }
191             }
192 
193     override val isEmptyState: Flow<Boolean> =
194         communalInteractor.widgetContent
195             .map { it.isEmpty() }
196             .distinctUntilChanged()
197             .onEach { logger.d("isEmptyState: $it") }
198 
199     private val _currentPopup: MutableStateFlow<PopupType?> = MutableStateFlow(null)
200     override val currentPopup: Flow<PopupType?> = _currentPopup.asStateFlow()
201 
202     // The widget is focusable for accessibility when the hub is fully visible and shade is not
203     // opened.
204     override val isFocusable: Flow<Boolean> =
205         combine(
206                 keyguardTransitionInteractor.isFinishedIn(
207                     content = Scenes.Communal,
208                     stateWithoutSceneContainer = KeyguardState.GLANCEABLE_HUB,
209                 ),
210                 communalInteractor.isIdleOnCommunal,
211                 shadeInteractor.isAnyFullyExpanded,
212             ) { transitionedToGlanceableHub, isIdleOnCommunal, isAnyFullyExpanded ->
213                 transitionedToGlanceableHub && isIdleOnCommunal && !isAnyFullyExpanded
214             }
215             .distinctUntilChanged()
216 
217     private val _isEnableWidgetDialogShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
218     val isEnableWidgetDialogShowing: Flow<Boolean> = _isEnableWidgetDialogShowing.asStateFlow()
219 
220     private val _isEnableWorkProfileDialogShowing: MutableStateFlow<Boolean> =
221         MutableStateFlow(false)
222     val isEnableWorkProfileDialogShowing: Flow<Boolean> =
223         _isEnableWorkProfileDialogShowing.asStateFlow()
224 
225     val isUiBlurred: StateFlow<Boolean> =
226         if (Flags.bouncerUiRevamp()) {
227             keyguardInteractor.primaryBouncerShowing
228         } else {
229             MutableStateFlow(false)
230         }
231 
232     val blurRadiusPx: Float = blurConfig.maxBlurRadiusPx
233 
234     init {
235         // Initialize our media host for the UMO. This only needs to happen once and must be done
236         // before the MediaHierarchyManager attempts to move the UMO to the hub.
237         with(mediaHost) {
238             expansion = MediaHostState.EXPANDED
239             expandedMatchesParentHeight = true
240             if (v2FlagEnabled()) {
241                 // Only show active media to match lock screen, not resumable media, which can
242                 // persist
243                 // for up to 2 days.
244                 showsOnlyActiveMedia = true
245             } else {
246                 // Maintain old behavior on tablet until V2 flag rolls out.
247                 showsOnlyActiveMedia = false
248             }
249             falsingProtectionNeeded = false
250             disableScrolling = true
251             init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB)
252         }
253     }
254 
255     override fun onOpenWidgetEditor(shouldOpenWidgetPickerOnStart: Boolean) {
256         persistScrollPosition()
257         communalInteractor.showWidgetEditor(shouldOpenWidgetPickerOnStart)
258     }
259 
260     override fun onDismissCtaTile() {
261         scope.launch {
262             communalInteractor.dismissCtaTile()
263             setCurrentPopupType(PopupType.CtaTile)
264         }
265     }
266 
267     override fun onShowPreviousMedia() {
268         mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1)
269     }
270 
271     override fun onShowNextMedia() {
272         mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1)
273     }
274 
275     override fun onTapWidget(componentName: ComponentName, rank: Int) {
276         metricsLogger.logTapWidget(componentName.flattenToString(), rank)
277     }
278 
279     fun onClick() {
280         keyguardIndicationController.showActionToUnlock()
281     }
282 
283     override fun onLongClick() {
284         if (Flags.glanceableHubDirectEditMode()) {
285             onOpenWidgetEditor(false)
286             return
287         }
288         setCurrentPopupType(PopupType.CustomizeWidgetButton)
289     }
290 
291     override fun onHidePopup() {
292         setCurrentPopupType(null)
293     }
294 
295     override fun onOpenEnableWidgetDialog() {
296         setIsEnableWidgetDialogShowing(true)
297     }
298 
299     fun onEnableWidgetDialogConfirm() {
300         communalInteractor.navigateToCommunalWidgetSettings()
301         setIsEnableWidgetDialogShowing(false)
302     }
303 
304     fun onEnableWidgetDialogCancel() {
305         setIsEnableWidgetDialogShowing(false)
306     }
307 
308     override fun onOpenEnableWorkProfileDialog() {
309         setIsEnableWorkProfileDialogShowing(true)
310     }
311 
312     fun onEnableWorkProfileDialogConfirm() {
313         communalInteractor.unpauseWorkProfile()
314         setIsEnableWorkProfileDialogShowing(false)
315     }
316 
317     fun onEnableWorkProfileDialogCancel() {
318         setIsEnableWorkProfileDialogShowing(false)
319     }
320 
321     private fun setIsEnableWidgetDialogShowing(isVisible: Boolean) {
322         _isEnableWidgetDialogShowing.value = isVisible
323     }
324 
325     private fun setIsEnableWorkProfileDialogShowing(isVisible: Boolean) {
326         _isEnableWorkProfileDialogShowing.value = isVisible
327     }
328 
329     private fun setCurrentPopupType(popupType: PopupType?) {
330         _currentPopup.value = popupType
331         delayedHideCurrentPopupJob?.cancel()
332 
333         if (popupType != null) {
334             delayedHideCurrentPopupJob =
335                 scope.launch {
336                     delay(POPUP_AUTO_HIDE_TIMEOUT_MS)
337                     setCurrentPopupType(null)
338                 }
339         } else {
340             delayedHideCurrentPopupJob = null
341         }
342     }
343 
344     private var delayedHideCurrentPopupJob: Job? = null
345 
346     /** Whether we can transition to a new scene based on a user gesture. */
347     fun canChangeScene(): Boolean {
348         return !shadeInteractor.isAnyFullyExpanded.value
349     }
350 
351     /**
352      * Whether touches should be disabled in communal.
353      *
354      * This is needed because the notification shade does not block touches in blank areas and these
355      * fall through to the glanceable hub, which we don't want.
356      *
357      * Using a StateFlow as the value does not necessarily change when hub becomes available.
358      */
359     val touchesAllowed: StateFlow<Boolean> =
360         not(shadeInteractor.isAnyFullyExpanded)
361             .stateIn(bgScope, SharingStarted.Eagerly, initialValue = false)
362 
363     /** The type of background to use for the hub. */
364     val communalBackground: Flow<CommunalBackgroundType> =
365         communalSettingsInteractor.communalBackground
366 
367     /** See [CommunalSettingsInteractor.isV2FlagEnabled] */
368     fun v2FlagEnabled(): Boolean = communalSettingsInteractor.isV2FlagEnabled()
369 
370     val swipeToHubEnabled: Flow<Boolean> by lazy {
371         val inAllowedDeviceState =
372             if (v2FlagEnabled()) {
373                 communalSettingsInteractor.manualOpenEnabled
374             } else {
375                 MutableStateFlow(swipeToHub)
376             }
377 
378         if (v2FlagEnabled()) {
379             val inAllowedKeyguardState =
380                 keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
381                     it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB
382                 }
383             allOf(inAllowedDeviceState, inAllowedKeyguardState)
384         } else {
385             inAllowedDeviceState
386         }
387     }
388 
389     val swipeFromHubInLandscape: Flow<Boolean> = communalSceneInteractor.willRotateToPortrait
390 
391     fun onOrientationChange(orientation: Int) =
392         communalSceneInteractor.setCommunalContainerOrientation(orientation)
393 
394     companion object {
395         const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L
396     }
397 }
398 
399 sealed class PopupType {
400     data object CtaTile : PopupType()
401 
402     data object CustomizeWidgetButton : PopupType()
403 }
404