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