• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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 package com.android.systemui.shade.ui.composable
18 
19 import android.view.ViewGroup
20 import androidx.compose.animation.core.animateDpAsState
21 import androidx.compose.animation.core.animateFloatAsState
22 import androidx.compose.animation.core.tween
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.clickable
25 import androidx.compose.foundation.clipScrollableContainer
26 import androidx.compose.foundation.gestures.Orientation
27 import androidx.compose.foundation.layout.Arrangement
28 import androidx.compose.foundation.layout.Box
29 import androidx.compose.foundation.layout.Column
30 import androidx.compose.foundation.layout.Row
31 import androidx.compose.foundation.layout.WindowInsets
32 import androidx.compose.foundation.layout.asPaddingValues
33 import androidx.compose.foundation.layout.displayCutout
34 import androidx.compose.foundation.layout.fillMaxHeight
35 import androidx.compose.foundation.layout.fillMaxSize
36 import androidx.compose.foundation.layout.fillMaxWidth
37 import androidx.compose.foundation.layout.height
38 import androidx.compose.foundation.layout.navigationBars
39 import androidx.compose.foundation.layout.padding
40 import androidx.compose.foundation.layout.systemBars
41 import androidx.compose.foundation.overscroll
42 import androidx.compose.foundation.rememberScrollState
43 import androidx.compose.foundation.verticalScroll
44 import androidx.compose.runtime.Composable
45 import androidx.compose.runtime.DisposableEffect
46 import androidx.compose.runtime.LaunchedEffect
47 import androidx.compose.runtime.getValue
48 import androidx.compose.runtime.mutableIntStateOf
49 import androidx.compose.runtime.remember
50 import androidx.compose.runtime.setValue
51 import androidx.compose.ui.Alignment
52 import androidx.compose.ui.Modifier
53 import androidx.compose.ui.graphics.CompositingStrategy
54 import androidx.compose.ui.graphics.graphicsLayer
55 import androidx.compose.ui.layout.Layout
56 import androidx.compose.ui.layout.layoutId
57 import androidx.compose.ui.platform.LocalContext
58 import androidx.compose.ui.platform.LocalDensity
59 import androidx.compose.ui.res.colorResource
60 import androidx.compose.ui.res.dimensionResource
61 import androidx.compose.ui.unit.dp
62 import androidx.compose.ui.zIndex
63 import androidx.lifecycle.compose.LocalLifecycleOwner
64 import androidx.lifecycle.compose.collectAsStateWithLifecycle
65 import com.android.compose.animation.scene.ContentScope
66 import com.android.compose.animation.scene.ElementKey
67 import com.android.compose.animation.scene.LowestZIndexContentPicker
68 import com.android.compose.animation.scene.UserAction
69 import com.android.compose.animation.scene.UserActionResult
70 import com.android.compose.animation.scene.animateContentDpAsState
71 import com.android.compose.animation.scene.animateContentFloatAsState
72 import com.android.compose.animation.scene.animateSceneFloatAsState
73 import com.android.compose.animation.scene.content.state.TransitionState
74 import com.android.compose.modifiers.padding
75 import com.android.compose.modifiers.thenIf
76 import com.android.internal.jank.InteractionJankMonitor
77 import com.android.systemui.battery.BatteryMeterViewController
78 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
79 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
80 import com.android.systemui.compose.modifiers.sysuiResTag
81 import com.android.systemui.dagger.SysUISingleton
82 import com.android.systemui.lifecycle.ExclusiveActivatable
83 import com.android.systemui.lifecycle.rememberViewModel
84 import com.android.systemui.media.controls.ui.composable.MediaCarousel
85 import com.android.systemui.media.controls.ui.composable.MediaContentPicker
86 import com.android.systemui.media.controls.ui.composable.isLandscape
87 import com.android.systemui.media.controls.ui.composable.shouldElevateMedia
88 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
89 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
90 import com.android.systemui.media.controls.ui.view.MediaHost
91 import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED
92 import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED
93 import com.android.systemui.media.dagger.MediaModule.QS_PANEL
94 import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
95 import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
96 import com.android.systemui.notifications.ui.composable.NotificationStackCutoffGuideline
97 import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
98 import com.android.systemui.qs.ui.composable.BrightnessMirror
99 import com.android.systemui.qs.ui.composable.QuickSettings
100 import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
101 import com.android.systemui.res.R
102 import com.android.systemui.scene.session.ui.composable.SaveableSession
103 import com.android.systemui.scene.shared.model.Scenes
104 import com.android.systemui.scene.ui.composable.Scene
105 import com.android.systemui.shade.shared.model.ShadeMode
106 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
107 import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel
108 import com.android.systemui.shade.ui.viewmodel.ShadeUserActionsViewModel
109 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
110 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
111 import com.android.systemui.statusbar.phone.StatusBarLocation
112 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
113 import com.android.systemui.statusbar.phone.ui.TintedIconManager
114 import com.android.systemui.util.Utils
115 import dagger.Lazy
116 import javax.inject.Inject
117 import javax.inject.Named
118 import kotlin.math.roundToInt
119 import kotlinx.coroutines.flow.Flow
120 
121 object Shade {
122     object Elements {
123         val BackgroundScrim =
124             ElementKey("ShadeBackgroundScrim", contentPicker = LowestZIndexContentPicker)
125         val SplitShadeStartColumn = ElementKey("SplitShadeStartColumn")
126     }
127 
128     object Dimensions {
129         val HorizontalPadding = 16.dp
130     }
131 }
132 
133 /** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */
134 @SysUISingleton
135 class ShadeScene
136 @Inject
137 constructor(
138     private val shadeSession: SaveableSession,
139     private val notificationStackScrollView: Lazy<NotificationScrollView>,
140     private val actionsViewModelFactory: ShadeUserActionsViewModel.Factory,
141     private val contentViewModelFactory: ShadeSceneContentViewModel.Factory,
142     private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
143     private val tintedIconManagerFactory: TintedIconManager.Factory,
144     private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
145     private val statusBarIconController: StatusBarIconController,
146     private val mediaCarouselController: MediaCarouselController,
147     @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost,
148     @Named(QS_PANEL) private val qsMediaHost: MediaHost,
149     private val jankMonitor: InteractionJankMonitor,
150 ) : ExclusiveActivatable(), Scene {
151 
152     override val key = Scenes.Shade
153 
<lambda>null154     private val actionsViewModel: ShadeUserActionsViewModel by lazy {
155         actionsViewModelFactory.create()
156     }
157 
onActivatednull158     override suspend fun onActivated(): Nothing {
159         actionsViewModel.activate()
160     }
161 
162     override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
163 
164     @Composable
Contentnull165     override fun ContentScope.Content(modifier: Modifier) {
166         val viewModel =
167             rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() }
168         val headerViewModel =
169             rememberViewModel("ShadeScene-headerViewModel") {
170                 viewModel.shadeHeaderViewModelFactory.create()
171             }
172         val notificationsPlaceholderViewModel =
173             rememberViewModel("ShadeScene-notifPlaceholderViewModel") {
174                 notificationsPlaceholderViewModelFactory.create()
175             }
176         ShadeScene(
177             notificationStackScrollView.get(),
178             viewModel = viewModel,
179             headerViewModel = headerViewModel,
180             notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
181             createTintedIconManager = tintedIconManagerFactory::create,
182             createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
183             statusBarIconController = statusBarIconController,
184             mediaCarouselController = mediaCarouselController,
185             qqsMediaHost = qqsMediaHost,
186             qsMediaHost = qsMediaHost,
187             jankMonitor = jankMonitor,
188             modifier = modifier,
189             shadeSession = shadeSession,
190             usingCollapsedLandscapeMedia =
191                 Utils.useCollapsedMediaInLandscape(LocalContext.current.resources),
192         )
193     }
194 
195     init {
196         qqsMediaHost.expansion = EXPANDED
197         qqsMediaHost.showsOnlyActiveMedia = true
198         qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
199 
200         qsMediaHost.expansion = EXPANDED
201         qsMediaHost.showsOnlyActiveMedia = false
202         qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
203     }
204 }
205 
206 @Composable
ContentScopenull207 private fun ContentScope.ShadeScene(
208     notificationStackScrollView: NotificationScrollView,
209     viewModel: ShadeSceneContentViewModel,
210     headerViewModel: ShadeHeaderViewModel,
211     notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
212     createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
213     createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
214     statusBarIconController: StatusBarIconController,
215     mediaCarouselController: MediaCarouselController,
216     qqsMediaHost: MediaHost,
217     qsMediaHost: MediaHost,
218     jankMonitor: InteractionJankMonitor,
219     modifier: Modifier = Modifier,
220     shadeSession: SaveableSession,
221     usingCollapsedLandscapeMedia: Boolean,
222 ) {
223     val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle()
224     when (shadeMode) {
225         is ShadeMode.Single ->
226             SingleShade(
227                 notificationStackScrollView = notificationStackScrollView,
228                 viewModel = viewModel,
229                 headerViewModel = headerViewModel,
230                 notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
231                 mediaCarouselController = mediaCarouselController,
232                 mediaHost = qqsMediaHost,
233                 modifier = modifier,
234                 shadeSession = shadeSession,
235                 usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
236                 jankMonitor = jankMonitor,
237             )
238         is ShadeMode.Split ->
239             SplitShade(
240                 notificationStackScrollView = notificationStackScrollView,
241                 viewModel = viewModel,
242                 headerViewModel = headerViewModel,
243                 notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
244                 mediaCarouselController = mediaCarouselController,
245                 mediaHost = qsMediaHost,
246                 modifier = modifier,
247                 shadeSession = shadeSession,
248                 jankMonitor = jankMonitor,
249             )
250         is ShadeMode.Dual -> error("Dual shade is implemented separately as an overlay.")
251     }
252 }
253 
254 @Composable
ContentScopenull255 private fun ContentScope.SingleShade(
256     notificationStackScrollView: NotificationScrollView,
257     viewModel: ShadeSceneContentViewModel,
258     headerViewModel: ShadeHeaderViewModel,
259     notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
260     mediaCarouselController: MediaCarouselController,
261     mediaHost: MediaHost,
262     jankMonitor: InteractionJankMonitor,
263     modifier: Modifier = Modifier,
264     shadeSession: SaveableSession,
265     usingCollapsedLandscapeMedia: Boolean,
266 ) {
267     val cutoutLocation = LocalDisplayCutout.current.location
268     val cutoutInsets = WindowInsets.Companion.displayCutout
269     mediaHost.expansion = if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED
270 
271     var maxNotifScrimTop by remember { mutableIntStateOf(0) }
272     val tileSquishiness by
273         animateSceneFloatAsState(
274             value = 1f,
275             key = QuickSettings.SharedValues.TilesSquishiness,
276             canOverflow = false,
277         )
278     val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle()
279     val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle()
280     val isQsEnabled by viewModel.isQsEnabled.collectAsStateWithLifecycle()
281 
282     val shouldPunchHoleBehindScrim =
283         layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
284             layoutState.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
285     // Media is visible and we are in landscape on a small height screen
286     val mediaInRow = isMediaVisible && isLandscape()
287     val mediaOffset by
288         animateContentDpAsState(
289             value = QuickSettings.SharedValues.MediaOffset.inQqs(mediaInRow),
290             key = MediaLandscapeTopOffset,
291             canOverflow = false,
292         )
293     val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings)
294     val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
295 
296     val mediaOffsetProvider = remember {
297         ShadeMediaOffsetProvider.Qqs(
298             { @Suppress("UNUSED_EXPRESSION") tileSquishiness },
299             viewModel.qsSceneAdapter,
300         )
301     }
302     val shadeHorizontalPadding =
303         dimensionResource(id = R.dimen.notification_panel_margin_horizontal)
304     val shadeMeasurePolicy =
305         remember(mediaInRow) {
306             SingleShadeMeasurePolicy(
307                 isMediaInRow = mediaInRow,
308                 mediaOffset = { mediaOffset.roundToPx() },
309                 onNotificationsTopChanged = { maxNotifScrimTop = it },
310                 mediaZIndex = {
311                     if (MediaContentPicker.shouldElevateMedia(layoutState)) 1f else 0f
312                 },
313                 cutoutInsetsProvider = {
314                     if (cutoutLocation == CutoutLocation.CENTER) {
315                         null
316                     } else {
317                         cutoutInsets
318                     }
319                 },
320             )
321         }
322 
323     Box(
324         modifier =
325             modifier.thenIf(shouldPunchHoleBehindScrim) {
326                 // Render the scene to an offscreen buffer so that BlendMode.DstOut only clears this
327                 // scene (and not the one under it) during a scene transition.
328                 Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
329             }
330     ) {
331         Box(
332             modifier =
333                 Modifier.fillMaxSize()
334                     .element(Shade.Elements.BackgroundScrim)
335                     .background(colorResource(R.color.shade_scrim_background_dark))
336         )
337         Layout(
338             modifier =
339                 Modifier.thenIf(isEmptySpaceClickable) {
340                     Modifier.clickable { viewModel.onEmptySpaceClicked() }
341                 },
342             content = {
343                 CollapsedShadeHeader(
344                     viewModel = headerViewModel,
345                     isSplitShade = false,
346                     modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
347                 )
348 
349                 Box(
350                     Modifier.element(QuickSettings.Elements.QuickQuickSettings)
351                         .layoutId(SingleShadeMeasurePolicy.LayoutId.QuickSettings)
352                         .padding(horizontal = shadeHorizontalPadding)
353                 ) {
354                     QuickSettings(
355                         viewModel.qsSceneAdapter,
356                         { viewModel.qsSceneAdapter.qqsHeight },
357                         isSplitShade = false,
358                         squishiness = { tileSquishiness },
359                     )
360                 }
361 
362                 val qqsLayoutPaddingBottom =
363                     dimensionResource(id = R.dimen.qqs_layout_padding_bottom)
364                 ShadeMediaCarousel(
365                     isVisible = isMediaVisible,
366                     isInRow = mediaInRow,
367                     mediaHost = mediaHost,
368                     mediaOffsetProvider = mediaOffsetProvider,
369                     carouselController = mediaCarouselController,
370                     modifier =
371                         Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Media)
372                             .padding(
373                                 horizontal =
374                                     shadeHorizontalPadding +
375                                         dimensionResource(id = R.dimen.qs_horizontal_margin)
376                             )
377                             .thenIf(!mediaInRow) {
378                                 Modifier.padding(bottom = qqsLayoutPaddingBottom)
379                             },
380                     usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
381                     isQsEnabled = isQsEnabled,
382                     isInSplitShade = false,
383                 )
384 
385                 NotificationScrollingStack(
386                     shadeSession = shadeSession,
387                     stackScrollView = notificationStackScrollView,
388                     viewModel = notificationsPlaceholderViewModel,
389                     jankMonitor = jankMonitor,
390                     maxScrimTop = { maxNotifScrimTop.toFloat() },
391                     shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
392                     stackTopPadding = notificationStackPadding,
393                     stackBottomPadding = navBarHeight,
394                     supportNestedScrolling = true,
395                     onEmptySpaceClick =
396                         viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
397                     modifier =
398                         Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Notifications)
399                             .padding(horizontal = shadeHorizontalPadding),
400                 )
401             },
402             measurePolicy = shadeMeasurePolicy,
403         )
404         Box(
405             modifier =
406                 Modifier.align(Alignment.BottomCenter)
407                     .height(navBarHeight)
408                     // Intercepts touches, prevents the scrollable container behind from scrolling.
409                     .clickable(interactionSource = null, indication = null) { /* do nothing */ }
410         ) {
411             NotificationStackCutoffGuideline(
412                 stackScrollView = notificationStackScrollView,
413                 viewModel = notificationsPlaceholderViewModel,
414                 modifier = Modifier.align(Alignment.TopCenter),
415             )
416         }
417     }
418 }
419 
420 @Composable
SplitShadenull421 private fun ContentScope.SplitShade(
422     notificationStackScrollView: NotificationScrollView,
423     viewModel: ShadeSceneContentViewModel,
424     headerViewModel: ShadeHeaderViewModel,
425     notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
426     mediaCarouselController: MediaCarouselController,
427     mediaHost: MediaHost,
428     modifier: Modifier = Modifier,
429     shadeSession: SaveableSession,
430     jankMonitor: InteractionJankMonitor,
431 ) {
432     val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle()
433     val isQsEnabled by viewModel.isQsEnabled.collectAsStateWithLifecycle()
434     val isCustomizerShowing by
435         viewModel.qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
436     val customizingAnimationDuration by
437         viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsStateWithLifecycle()
438     val lifecycleOwner = LocalLifecycleOwner.current
439     val footerActionsViewModel =
440         remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
441     val tileSquishiness by
442         animateContentFloatAsState(
443             value = 1f,
444             key = QuickSettings.SharedValues.TilesSquishiness,
445             canOverflow = false,
446         )
447     val unfoldTranslationXForStartSide by
448         viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f)
449 
450     val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings)
451     val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
452     val bottomPadding by
453         animateDpAsState(
454             targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
455             animationSpec = tween(customizingAnimationDuration),
456             label = "animateQSSceneBottomPaddingAsState",
457         )
458     val density = LocalDensity.current
459     LaunchedEffect(navBarBottomHeight, density) {
460         with(density) {
461             viewModel.qsSceneAdapter.applyBottomNavBarPadding(navBarBottomHeight.roundToPx())
462         }
463     }
464 
465     val quickSettingsScrollState = rememberScrollState()
466     val isScrollable = layoutState.transitionState is TransitionState.Idle
467     LaunchedEffect(isCustomizing, quickSettingsScrollState) {
468         if (isCustomizing) {
469             quickSettingsScrollState.scrollTo(0)
470         }
471     }
472 
473     val brightnessMirrorViewModel =
474         rememberViewModel("SplitShade-brightnessMirrorViewModel") {
475             viewModel.brightnessMirrorViewModelFactory.create()
476         }
477     val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
478     val contentAlpha by
479         animateFloatAsState(
480             targetValue = if (brightnessMirrorShowing) 0f else 1f,
481             label = "alphaAnimationBrightnessMirrorContentHiding",
482         )
483 
484     notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(contentAlpha)
485     DisposableEffect(Unit) {
486         onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) }
487     }
488 
489     val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle()
490     val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle()
491 
492     val brightnessMirrorShowingModifier = Modifier.graphicsLayer { alpha = contentAlpha }
493 
494     val mediaOffsetProvider = remember {
495         ShadeMediaOffsetProvider.Qs(
496             { @Suppress("UNUSED_EXPRESSION") tileSquishiness },
497             viewModel.qsSceneAdapter,
498         )
499     }
500 
501     Box {
502         Box(
503             modifier =
504                 modifier
505                     .fillMaxSize()
506                     .element(Shade.Elements.BackgroundScrim)
507                     // Cannot set the alpha of the whole element to 0, because the mirror should be
508                     // in the QS column.
509                     .background(
510                         colorResource(R.color.shade_scrim_background_dark)
511                             .copy(alpha = contentAlpha)
512                     )
513         )
514 
515         Column(modifier = Modifier.fillMaxSize()) {
516             CollapsedShadeHeader(
517                 viewModel = headerViewModel,
518                 isSplitShade = true,
519                 modifier =
520                     Modifier.then(brightnessMirrorShowingModifier)
521                         .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }),
522             )
523 
524             Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
525                 Box(
526                     modifier =
527                         Modifier.element(Shade.Elements.SplitShadeStartColumn)
528                             .overscroll(verticalOverscrollEffect)
529                             .weight(1f)
530                             .graphicsLayer { translationX = unfoldTranslationXForStartSide }
531                 ) {
532                     Box(modifier = Modifier.fillMaxSize()) {
533                         BrightnessMirror(
534                             viewModel = brightnessMirrorViewModel,
535                             qsSceneAdapter = viewModel.qsSceneAdapter,
536                             modifier = Modifier.align(Alignment.TopCenter),
537                             measureFromContainer = true,
538                         )
539                     }
540                     Column(
541                         verticalArrangement = Arrangement.Top,
542                         modifier = Modifier.fillMaxSize().padding(bottom = bottomPadding),
543                     ) {
544                         Column(
545                             modifier =
546                                 Modifier.fillMaxSize()
547                                     .sysuiResTag("expanded_qs_scroll_view")
548                                     .weight(1f)
549                                     .thenIf(!isCustomizerShowing) {
550                                         Modifier.verticalScroll(
551                                                 quickSettingsScrollState,
552                                                 enabled = isScrollable,
553                                             )
554                                             .clipScrollableContainer(Orientation.Horizontal)
555                                     }
556                                     .then(brightnessMirrorShowingModifier)
557                         ) {
558                             Box(
559                                 modifier =
560                                     Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings)
561                             ) {
562                                 QuickSettings(
563                                     qsSceneAdapter = viewModel.qsSceneAdapter,
564                                     heightProvider = { viewModel.qsSceneAdapter.qsHeight },
565                                     isSplitShade = true,
566                                     modifier = Modifier.fillMaxWidth(),
567                                     squishiness = { tileSquishiness },
568                                 )
569                             }
570 
571                             ShadeMediaCarousel(
572                                 isVisible = isMediaVisible,
573                                 isInRow = false,
574                                 mediaHost = mediaHost,
575                                 mediaOffsetProvider = mediaOffsetProvider,
576                                 modifier =
577                                     Modifier.thenIf(
578                                             MediaContentPicker.shouldElevateMedia(layoutState)
579                                         ) {
580                                             Modifier.zIndex(1f)
581                                         }
582                                         .padding(
583                                             horizontal =
584                                                 dimensionResource(id = R.dimen.qs_horizontal_margin)
585                                         ),
586                                 carouselController = mediaCarouselController,
587                                 isQsEnabled = isQsEnabled,
588                                 isInSplitShade = true,
589                             )
590                         }
591                         FooterActionsWithAnimatedVisibility(
592                             viewModel = footerActionsViewModel,
593                             isCustomizing = isCustomizing,
594                             customizingAnimationDuration = customizingAnimationDuration,
595                             lifecycleOwner = lifecycleOwner,
596                             modifier =
597                                 Modifier.align(Alignment.CenterHorizontally)
598                                     .sysuiResTag("qs_footer_actions")
599                                     .then(brightnessMirrorShowingModifier),
600                         )
601                     }
602                 }
603 
604                 NotificationScrollingStack(
605                     shadeSession = shadeSession,
606                     stackScrollView = notificationStackScrollView,
607                     viewModel = notificationsPlaceholderViewModel,
608                     jankMonitor = jankMonitor,
609                     maxScrimTop = { 0f },
610                     stackTopPadding = notificationStackPadding,
611                     stackBottomPadding = notificationStackPadding,
612                     shouldPunchHoleBehindScrim = false,
613                     supportNestedScrolling = false,
614                     onEmptySpaceClick =
615                         viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
616                     modifier =
617                         Modifier.weight(1f)
618                             .fillMaxHeight()
619                             .padding(
620                                 end =
621                                     dimensionResource(R.dimen.notification_panel_margin_horizontal),
622                                 bottom = navBarBottomHeight,
623                             )
624                             .then(brightnessMirrorShowingModifier),
625                 )
626             }
627         }
628         NotificationStackCutoffGuideline(
629             stackScrollView = notificationStackScrollView,
630             viewModel = notificationsPlaceholderViewModel,
631             modifier =
632                 Modifier.align(Alignment.BottomCenter)
633                     .padding(bottom = notificationStackPadding + navBarBottomHeight),
634         )
635     }
636 }
637 
638 @Composable
ContentScopenull639 private fun ContentScope.ShadeMediaCarousel(
640     isVisible: Boolean,
641     isInRow: Boolean,
642     mediaHost: MediaHost,
643     carouselController: MediaCarouselController,
644     mediaOffsetProvider: ShadeMediaOffsetProvider,
645     isInSplitShade: Boolean,
646     isQsEnabled: Boolean,
647     modifier: Modifier = Modifier,
648     usingCollapsedLandscapeMedia: Boolean = false,
649 ) {
650     if (!isQsEnabled) {
651         return
652     }
653     MediaCarousel(
654         modifier = modifier.fillMaxWidth(),
655         isVisible = isVisible,
656         mediaHost = mediaHost,
657         carouselController = carouselController,
658         offsetProvider =
659             if (isInRow || MediaContentPicker.shouldElevateMedia(layoutState)) {
660                 null
661             } else {
662                 { mediaOffsetProvider.offset }
663             },
664         usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
665         isInSplitShade = isInSplitShade,
666     )
667 }
668