1 /*
<lambda>null2  * Copyright 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.scene.ui.view
18 
19 import android.content.Context
20 import android.graphics.Point
21 import android.view.View
22 import android.view.ViewGroup
23 import android.view.WindowInsets
24 import androidx.activity.OnBackPressedDispatcher
25 import androidx.activity.OnBackPressedDispatcherOwner
26 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
27 import androidx.compose.ui.platform.ComposeView
28 import androidx.compose.ui.unit.Dp
29 import androidx.compose.ui.unit.dp
30 import androidx.core.view.isVisible
31 import androidx.lifecycle.Lifecycle
32 import androidx.lifecycle.lifecycleScope
33 import androidx.lifecycle.repeatOnLifecycle
34 import com.android.compose.animation.scene.SceneKey
35 import com.android.compose.theme.PlatformTheme
36 import com.android.internal.policy.ScreenDecorationsUtils
37 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
38 import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout
39 import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider
40 import com.android.systemui.lifecycle.repeatWhenAttached
41 import com.android.systemui.res.R
42 import com.android.systemui.scene.shared.flag.SceneContainerFlag
43 import com.android.systemui.scene.shared.model.Scene
44 import com.android.systemui.scene.shared.model.SceneContainerConfig
45 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
46 import com.android.systemui.scene.ui.composable.ComposableScene
47 import com.android.systemui.scene.ui.composable.SceneContainer
48 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
49 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
50 import kotlinx.coroutines.CoroutineScope
51 import kotlinx.coroutines.flow.SharingStarted
52 import kotlinx.coroutines.flow.StateFlow
53 import kotlinx.coroutines.flow.map
54 import kotlinx.coroutines.flow.stateIn
55 import kotlinx.coroutines.launch
56 
57 object SceneWindowRootViewBinder {
58 
59     /** Binds between the view and view-model pertaining to a specific scene container. */
60     fun bind(
61         view: ViewGroup,
62         viewModel: SceneContainerViewModel,
63         windowInsets: StateFlow<WindowInsets?>,
64         containerConfig: SceneContainerConfig,
65         sharedNotificationContainer: SharedNotificationContainer,
66         scenes: Set<Scene>,
67         onVisibilityChangedInternal: (isVisible: Boolean) -> Unit,
68         dataSourceDelegator: SceneDataSourceDelegator,
69     ) {
70         val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
71         val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
72             containerConfig.sceneKeys.forEach { sceneKey ->
73                 val scene =
74                     checkNotNull(unsortedSceneByKey[sceneKey]) {
75                         "Scene not found for key \"$sceneKey\"!"
76                     }
77 
78                 put(sceneKey, scene)
79             }
80         }
81 
82         view.repeatWhenAttached {
83             lifecycleScope.launch {
84                 repeatOnLifecycle(Lifecycle.State.CREATED) {
85                     view.setViewTreeOnBackPressedDispatcherOwner(
86                         object : OnBackPressedDispatcherOwner {
87                             override val onBackPressedDispatcher =
88                                 OnBackPressedDispatcher().apply {
89                                     setOnBackInvokedDispatcher(
90                                         view.viewRootImpl.onBackInvokedDispatcher
91                                     )
92                                 }
93 
94                             override val lifecycle: Lifecycle = this@repeatWhenAttached.lifecycle
95                         }
96                     )
97 
98                     view.addView(
99                         createSceneContainerView(
100                                 scope = this,
101                                 context = view.context,
102                                 viewModel = viewModel,
103                                 windowInsets = windowInsets,
104                                 sceneByKey = sortedSceneByKey,
105                                 dataSourceDelegator = dataSourceDelegator,
106                             )
107                             .also { it.id = R.id.scene_container_root_composable }
108                     )
109 
110                     val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
111                     legacyView.isVisible = false
112 
113                     // This moves the SharedNotificationContainer to the WindowRootView just after
114                     //  the SceneContainerView. This SharedNotificationContainer should contain NSSL
115                     //  due to the NotificationStackScrollLayoutSection (legacy) or
116                     //  NotificationSection (scene container) moving it there.
117                     if (SceneContainerFlag.isEnabled) {
118                         (sharedNotificationContainer.parent as? ViewGroup)?.removeView(
119                             sharedNotificationContainer
120                         )
121                         view.addView(sharedNotificationContainer)
122                     }
123 
124                     launch {
125                         viewModel.isVisible.collect { isVisible ->
126                             onVisibilityChangedInternal(isVisible)
127                         }
128                     }
129                 }
130 
131                 // Here when destroyed.
132                 view.removeAllViews()
133             }
134         }
135     }
136 
137     private fun createSceneContainerView(
138         scope: CoroutineScope,
139         context: Context,
140         viewModel: SceneContainerViewModel,
141         windowInsets: StateFlow<WindowInsets?>,
142         sceneByKey: Map<SceneKey, Scene>,
143         dataSourceDelegator: SceneDataSourceDelegator,
144     ): View {
145         return ComposeView(context).apply {
146             setContent {
147                 PlatformTheme {
148                     ScreenDecorProvider(
149                         displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
150                         screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
151                     ) {
152                         SceneContainer(
153                             viewModel = viewModel,
154                             sceneByKey =
155                                 sceneByKey.mapValues { (_, scene) -> scene as ComposableScene },
156                             dataSourceDelegator = dataSourceDelegator,
157                         )
158                     }
159                 }
160             }
161         }
162     }
163 
164     // TODO(b/298525212): remove once Compose exposes window inset bounds.
165     private fun displayCutoutFromWindowInsets(
166         scope: CoroutineScope,
167         context: Context,
168         windowInsets: StateFlow<WindowInsets?>,
169     ): StateFlow<DisplayCutout> =
170         windowInsets
171             .map {
172                 val boundingRect = it?.displayCutout?.boundingRectTop
173                 val width = boundingRect?.let { boundingRect.right - boundingRect.left } ?: 0
174                 val left = boundingRect?.left?.toDp(context) ?: 0.dp
175                 val top = boundingRect?.top?.toDp(context) ?: 0.dp
176                 val right = boundingRect?.right?.toDp(context) ?: 0.dp
177                 val bottom = boundingRect?.bottom?.toDp(context) ?: 0.dp
178                 val location =
179                     when {
180                         width <= 0f -> CutoutLocation.NONE
181                         left <= 0.dp -> CutoutLocation.LEFT
182                         right >= getDisplayWidth(context) -> CutoutLocation.RIGHT
183                         else -> CutoutLocation.CENTER
184                     }
185                 val viewDisplayCutout = it?.displayCutout
186                 DisplayCutout(
187                     left,
188                     top,
189                     right,
190                     bottom,
191                     location,
192                     viewDisplayCutout,
193                 )
194             }
195             .stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout())
196 
197     // TODO(b/298525212): remove once Compose exposes window inset bounds.
198     private fun getDisplayWidth(context: Context): Dp {
199         val point = Point()
200         checkNotNull(context.display).getRealSize(point)
201         return point.x.toDp(context)
202     }
203 
204     // TODO(b/298525212): remove once Compose exposes window inset bounds.
205     private fun Int.toDp(context: Context): Dp {
206         return (this.toFloat() / context.resources.displayMetrics.density).dp
207     }
208 }
209