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