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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.shade.ui.viewmodel
20 
21 import androidx.lifecycle.LifecycleOwner
22 import com.android.compose.animation.scene.SceneKey
23 import com.android.compose.animation.scene.Swipe
24 import com.android.compose.animation.scene.SwipeDirection
25 import com.android.compose.animation.scene.UserAction
26 import com.android.compose.animation.scene.UserActionResult
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Application
29 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
30 import com.android.systemui.qs.FooterActionsController
31 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
32 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
33 import com.android.systemui.scene.domain.interactor.SceneInteractor
34 import com.android.systemui.scene.shared.model.SceneFamilies
35 import com.android.systemui.scene.shared.model.Scenes
36 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
37 import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
38 import com.android.systemui.shade.domain.interactor.ShadeInteractor
39 import com.android.systemui.shade.shared.model.ShadeMode
40 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
41 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
42 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
43 import java.util.concurrent.atomic.AtomicBoolean
44 import javax.inject.Inject
45 import kotlinx.coroutines.CoroutineScope
46 import kotlinx.coroutines.ExperimentalCoroutinesApi
47 import kotlinx.coroutines.flow.Flow
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.flowOf
52 import kotlinx.coroutines.flow.map
53 import kotlinx.coroutines.flow.stateIn
54 
55 /** Models UI state and handles user input for the shade scene. */
56 @SysUISingleton
57 class ShadeSceneViewModel
58 @Inject
59 constructor(
60     @Application private val applicationScope: CoroutineScope,
61     val qsSceneAdapter: QSSceneAdapter,
62     val shadeHeaderViewModel: ShadeHeaderViewModel,
63     val notifications: NotificationsPlaceholderViewModel,
64     val brightnessMirrorViewModel: BrightnessMirrorViewModel,
65     val mediaCarouselInteractor: MediaCarouselInteractor,
66     shadeInteractor: ShadeInteractor,
67     private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
68     private val footerActionsController: FooterActionsController,
69     private val sceneInteractor: SceneInteractor,
70     private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
71 ) {
72     val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
73         combine(
74                 shadeInteractor.shadeMode,
75                 qsSceneAdapter.isCustomizerShowing,
76             ) { shadeMode, isCustomizerShowing ->
77                 destinationScenes(
78                     shadeMode = shadeMode,
79                     isCustomizing = isCustomizerShowing,
80                 )
81             }
82             .stateIn(
83                 scope = applicationScope,
84                 started = SharingStarted.WhileSubscribed(),
85                 initialValue =
86                     destinationScenes(
87                         shadeMode = shadeInteractor.shadeMode.value,
88                         isCustomizing = qsSceneAdapter.isCustomizerShowing.value,
89                     ),
90             )
91 
92     private val upDestinationSceneKey: Flow<SceneKey?> =
93         destinationScenes.map { it[Swipe(SwipeDirection.Up)]?.toScene }
94 
95     /** Whether or not the shade container should be clickable. */
96     val isClickable: StateFlow<Boolean> =
97         upDestinationSceneKey
98             .flatMapLatestConflated { key ->
99                 key?.let { sceneInteractor.resolveSceneFamily(key) } ?: flowOf(null)
100             }
101             .map { it == Scenes.Lockscreen }
102             .stateIn(
103                 scope = applicationScope,
104                 started = SharingStarted.WhileSubscribed(),
105                 initialValue = false
106             )
107 
108     val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
109 
110     val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation
111 
112     /**
113      * Amount of X-axis translation to apply to various elements as the unfolded foldable is folded
114      * slightly, in pixels.
115      */
116     fun unfoldTranslationX(isOnStartSide: Boolean): Flow<Float> {
117         return unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide)
118     }
119 
120     /** Notifies that some content in the shade was clicked. */
121     fun onContentClicked() {
122         if (!isClickable.value) {
123             return
124         }
125 
126         sceneInteractor.changeScene(Scenes.Lockscreen, "Shade empty content clicked")
127     }
128 
129     private val footerActionsControllerInitialized = AtomicBoolean(false)
130 
131     fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
132         if (footerActionsControllerInitialized.compareAndSet(false, true)) {
133             footerActionsController.init()
134         }
135         return footerActionsViewModelFactory.create(lifecycleOwner)
136     }
137 
138     private fun destinationScenes(
139         shadeMode: ShadeMode,
140         isCustomizing: Boolean,
141     ): Map<UserAction, UserActionResult> {
142         return buildMap {
143             if (!isCustomizing) {
144                 set(
145                     Swipe(SwipeDirection.Up),
146                     UserActionResult(
147                         SceneFamilies.Home,
148                         ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
149                     )
150                 )
151             } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
152             if (shadeMode is ShadeMode.Single) {
153                 set(Swipe(SwipeDirection.Down), UserActionResult(Scenes.QuickSettings))
154             }
155         }
156     }
157 }
158