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