• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.compose.animation.scene.OverlayKey
33 import com.android.compose.animation.scene.SceneKey
34 import com.android.compose.theme.PlatformTheme
35 import com.android.internal.policy.ScreenDecorationsUtils
36 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
37 import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout
38 import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider
39 import com.android.systemui.lifecycle.WindowLifecycleState
40 import com.android.systemui.lifecycle.repeatWhenAttached
41 import com.android.systemui.lifecycle.setSnapshotBinding
42 import com.android.systemui.lifecycle.viewModel
43 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
44 import com.android.systemui.res.R
45 import com.android.systemui.scene.shared.model.SceneContainerConfig
46 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
47 import com.android.systemui.scene.ui.composable.Overlay
48 import com.android.systemui.scene.ui.composable.Scene
49 import com.android.systemui.scene.ui.composable.SceneContainer
50 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
51 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
52 import javax.inject.Provider
53 import kotlinx.coroutines.CoroutineScope
54 import kotlinx.coroutines.awaitCancellation
55 import kotlinx.coroutines.flow.SharingStarted
56 import kotlinx.coroutines.flow.StateFlow
57 import kotlinx.coroutines.flow.map
58 import kotlinx.coroutines.flow.stateIn
59 
60 object SceneWindowRootViewBinder {
61 
62     /** Binds between the view and view-model pertaining to a specific scene container. */
63     fun bind(
64         view: ViewGroup,
65         viewModelFactory: SceneContainerViewModel.Factory,
66         motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit,
67         windowInsets: StateFlow<WindowInsets?>,
68         containerConfig: SceneContainerConfig,
69         sharedNotificationContainer: SharedNotificationContainer,
70         scenes: Set<Scene>,
71         overlays: Set<Overlay>,
72         onVisibilityChangedInternal: (isVisible: Boolean) -> Unit,
73         dataSourceDelegator: SceneDataSourceDelegator,
74         qsSceneAdapter: Provider<QSSceneAdapter>,
75         sceneJankMonitorFactory: SceneJankMonitor.Factory,
76     ) {
77         val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
78         val sortedSceneByKey: Map<SceneKey, Scene> =
79             LinkedHashMap<SceneKey, Scene>(containerConfig.sceneKeys.size).apply {
80                 containerConfig.sceneKeys.forEach { sceneKey ->
81                     val scene =
82                         checkNotNull(unsortedSceneByKey[sceneKey]) {
83                             "Scene not found for key \"$sceneKey\"!"
84                         }
85 
86                     put(sceneKey, scene)
87                 }
88             }
89 
90         val unsortedOverlayByKey: Map<OverlayKey, Overlay> =
91             overlays.associateBy { overlay -> overlay.key }
92         val sortedOverlayByKey: Map<OverlayKey, Overlay> =
93             LinkedHashMap<OverlayKey, Overlay>(containerConfig.overlayKeys.size).apply {
94                 containerConfig.overlayKeys.forEach { overlayKey ->
95                     val overlay =
96                         checkNotNull(unsortedOverlayByKey[overlayKey]) {
97                             "Overlay not found for key \"$overlayKey\"!"
98                         }
99 
100                     put(overlayKey, overlay)
101                 }
102             }
103 
104         view.repeatWhenAttached {
105             view.viewModel(
106                 traceName = "SceneWindowRootViewBinder",
107                 minWindowLifecycleState = WindowLifecycleState.ATTACHED,
108                 factory = { viewModelFactory.create(view, motionEventHandlerReceiver) },
109             ) { viewModel ->
110                 try {
111                     view.setViewTreeOnBackPressedDispatcherOwner(
112                         object : OnBackPressedDispatcherOwner {
113                             override val onBackPressedDispatcher =
114                                 OnBackPressedDispatcher().apply {
115                                     setOnBackInvokedDispatcher(
116                                         view.viewRootImpl.onBackInvokedDispatcher
117                                     )
118                                 }
119 
120                             override val lifecycle: Lifecycle = this@repeatWhenAttached.lifecycle
121                         }
122                     )
123 
124                     view.addView(
125                         createSceneContainerView(
126                                 scope = this,
127                                 context = view.context,
128                                 viewModel = viewModel,
129                                 windowInsets = windowInsets,
130                                 sceneByKey = sortedSceneByKey,
131                                 overlayByKey = sortedOverlayByKey,
132                                 dataSourceDelegator = dataSourceDelegator,
133                                 qsSceneAdapter = qsSceneAdapter,
134                                 containerConfig = containerConfig,
135                                 sceneJankMonitorFactory = sceneJankMonitorFactory,
136                             )
137                             .also { it.id = R.id.scene_container_root_composable }
138                     )
139 
140                     val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
141                     legacyView.isVisible = false
142 
143                     // This moves the SharedNotificationContainer to the WindowRootView just after
144                     //  the SceneContainerView. This SharedNotificationContainer should contain NSSL
145                     //  due to the NotificationStackScrollLayoutSection (legacy) or
146                     //  NotificationSection (scene container) moving it there.
147                     (sharedNotificationContainer.parent as? ViewGroup)?.removeView(
148                         sharedNotificationContainer
149                     )
150                     view.addView(sharedNotificationContainer)
151 
152                     view.setSnapshotBinding { onVisibilityChangedInternal(viewModel.isVisible) }
153                     awaitCancellation()
154                 } finally {
155                     // Here when destroyed.
156                     view.removeAllViews()
157                 }
158             }
159         }
160     }
161 
162     private fun createSceneContainerView(
163         scope: CoroutineScope,
164         context: Context,
165         viewModel: SceneContainerViewModel,
166         windowInsets: StateFlow<WindowInsets?>,
167         sceneByKey: Map<SceneKey, Scene>,
168         overlayByKey: Map<OverlayKey, Overlay>,
169         dataSourceDelegator: SceneDataSourceDelegator,
170         qsSceneAdapter: Provider<QSSceneAdapter>,
171         containerConfig: SceneContainerConfig,
172         sceneJankMonitorFactory: SceneJankMonitor.Factory,
173     ): View {
174         return ComposeView(context).apply {
175             setContent {
176                 PlatformTheme {
177                     ScreenDecorProvider(
178                         displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
179                         screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context),
180                     ) {
181                         SceneContainer(
182                             viewModel = viewModel,
183                             sceneByKey = sceneByKey,
184                             overlayByKey = overlayByKey,
185                             initialSceneKey = containerConfig.initialSceneKey,
186                             transitionsBuilder = containerConfig.transitionsBuilder,
187                             dataSourceDelegator = dataSourceDelegator,
188                             qsSceneAdapter = qsSceneAdapter,
189                             sceneJankMonitorFactory = sceneJankMonitorFactory,
190                         )
191                     }
192                 }
193             }
194         }
195     }
196 
197     // TODO(b/298525212): remove once Compose exposes window inset bounds.
198     private fun displayCutoutFromWindowInsets(
199         scope: CoroutineScope,
200         context: Context,
201         windowInsets: StateFlow<WindowInsets?>,
202     ): StateFlow<DisplayCutout> =
203         windowInsets
204             .map {
205                 val boundingRect = it?.displayCutout?.boundingRectTop
206                 val width = boundingRect?.let { boundingRect.right - boundingRect.left } ?: 0
207                 val left = boundingRect?.left?.toDp(context) ?: 0.dp
208                 val top = boundingRect?.top?.toDp(context) ?: 0.dp
209                 val right = boundingRect?.right?.toDp(context) ?: 0.dp
210                 val bottom = boundingRect?.bottom?.toDp(context) ?: 0.dp
211                 val location =
212                     when {
213                         width <= 0f -> CutoutLocation.NONE
214                         left <= 0.dp -> CutoutLocation.LEFT
215                         right >= getDisplayWidth(context) -> CutoutLocation.RIGHT
216                         else -> CutoutLocation.CENTER
217                     }
218                 val viewDisplayCutout = it?.displayCutout
219                 DisplayCutout(left, top, right, bottom, location, viewDisplayCutout)
220             }
221             .stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout())
222 
223     // TODO(b/298525212): remove once Compose exposes window inset bounds.
224     private fun getDisplayWidth(context: Context): Dp {
225         val point = Point()
226         checkNotNull(context.display).getRealSize(point)
227         return point.x.toDp(context)
228     }
229 
230     // TODO(b/298525212): remove once Compose exposes window inset bounds.
231     private fun Int.toDp(context: Context): Dp {
232         return (this.toFloat() / context.resources.displayMetrics.density).dp
233     }
234 }
235