• 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 @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
18 
19 package com.android.compose.animation.scene.demo
20 
21 import android.content.Context
22 import androidx.activity.compose.BackHandler
23 import androidx.compose.foundation.LocalOverscrollFactory
24 import androidx.compose.foundation.OverscrollEffect
25 import androidx.compose.foundation.border
26 import androidx.compose.foundation.clickable
27 import androidx.compose.foundation.horizontalScroll
28 import androidx.compose.foundation.layout.Arrangement
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.Column
31 import androidx.compose.foundation.layout.Row
32 import androidx.compose.foundation.layout.fillMaxHeight
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.foundation.pager.rememberPagerState
35 import androidx.compose.foundation.rememberScrollState
36 import androidx.compose.foundation.shape.RoundedCornerShape
37 import androidx.compose.material.icons.Icons
38 import androidx.compose.material.icons.filled.AirplanemodeInactive
39 import androidx.compose.material.icons.filled.Bedtime
40 import androidx.compose.material.icons.filled.Bluetooth
41 import androidx.compose.material.icons.filled.ChargingStation
42 import androidx.compose.material.icons.filled.CreditCard
43 import androidx.compose.material.icons.filled.DoNotDisturb
44 import androidx.compose.material.icons.filled.FlashlightOff
45 import androidx.compose.material.icons.filled.Home
46 import androidx.compose.material.icons.filled.NearbyOff
47 import androidx.compose.material.icons.filled.NetworkWifi
48 import androidx.compose.material.icons.filled.PowerSettingsNew
49 import androidx.compose.material.icons.filled.ScreenRotation
50 import androidx.compose.material.icons.filled.Settings
51 import androidx.compose.material.icons.filled.Videocam
52 import androidx.compose.material.icons.filled.ZoomOutMap
53 import androidx.compose.material3.Button
54 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
55 import androidx.compose.material3.Icon
56 import androidx.compose.material3.IconButton
57 import androidx.compose.material3.LocalContentColor
58 import androidx.compose.material3.MaterialTheme
59 import androidx.compose.material3.MotionScheme
60 import androidx.compose.material3.Surface
61 import androidx.compose.material3.Text
62 import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
63 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
64 import androidx.compose.material3.windowsizeclass.WindowSizeClass
65 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
66 import androidx.compose.runtime.Composable
67 import androidx.compose.runtime.CompositionLocalProvider
68 import androidx.compose.runtime.getValue
69 import androidx.compose.runtime.key
70 import androidx.compose.runtime.mutableStateOf
71 import androidx.compose.runtime.remember
72 import androidx.compose.runtime.rememberCoroutineScope
73 import androidx.compose.runtime.saveable.Saver
74 import androidx.compose.runtime.saveable.SaverScope
75 import androidx.compose.runtime.saveable.rememberSaveable
76 import androidx.compose.runtime.setValue
77 import androidx.compose.ui.Alignment
78 import androidx.compose.ui.Modifier
79 import androidx.compose.ui.draw.clip
80 import androidx.compose.ui.graphics.Color
81 import androidx.compose.ui.graphics.toComposeRect
82 import androidx.compose.ui.graphics.vector.ImageVector
83 import androidx.compose.ui.platform.LocalConfiguration
84 import androidx.compose.ui.platform.LocalContext
85 import androidx.compose.ui.platform.LocalDensity
86 import androidx.compose.ui.platform.LocalHapticFeedback
87 import androidx.compose.ui.platform.testTag
88 import androidx.compose.ui.semantics.semantics
89 import androidx.compose.ui.semantics.testTagsAsResourceId
90 import androidx.compose.ui.state.ToggleableState
91 import androidx.compose.ui.text.TextMeasurer
92 import androidx.compose.ui.text.rememberTextMeasurer
93 import androidx.compose.ui.unit.Density
94 import androidx.compose.ui.unit.dp
95 import androidx.window.layout.WindowMetricsCalculator
96 import com.android.compose.animation.scene.ContentScope
97 import com.android.compose.animation.scene.DefaultEdgeDetector
98 import com.android.compose.animation.scene.ElementKey
99 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
100 import com.android.compose.animation.scene.OverlayKey
101 import com.android.compose.animation.scene.SceneKey
102 import com.android.compose.animation.scene.SceneTransitionLayout
103 import com.android.compose.animation.scene.SceneTransitions
104 import com.android.compose.animation.scene.demo.notification.NotificationList
105 import com.android.compose.animation.scene.demo.notification.notifications
106 import com.android.compose.animation.scene.demo.transitions.systemUiTransitions
107 import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
108 import com.android.compose.modifiers.thenIf
109 import com.android.compose.windowsizeclass.calculateWindowSizeClass
110 import com.android.mechanics.behavior.VerticalExpandContainerSpec
111 import kotlin.math.max
112 
113 object Scenes {
114     val AlwaysOnDisplay = SceneKey("AlwaysOnDisplay")
115     val Bouncer = SceneKey("Bouncer")
116     val Camera = SceneKey("Camera")
117     val Launcher = SceneKey("Launcher")
118     val Lockscreen = SceneKey("Lockscreen")
119     val SplitLockscreen = SceneKey("SplitLockscreen")
120     val QuickSettings = SceneKey("QuickSettings")
121     val Shade = SceneKey("Shade")
122     val SplitShade = SceneKey("SplitShade")
123 
124     // Stub scenes on the start and end of the lockscreen.
125     val StubStart = SceneKey("StubStart")
126     val StubEnd = SceneKey("StubEnd")
127 
128     val AllScenes =
129         listOf(
130                 AlwaysOnDisplay,
131                 Bouncer,
132                 Camera,
133                 Launcher,
134                 Lockscreen,
135                 SplitLockscreen,
136                 QuickSettings,
137                 Shade,
138                 SplitShade,
139                 StubStart,
140                 StubEnd,
141             )
142             .associateBy { it.debugName }
143 
144     /**
145      * A smart saver that restores the right scene depending on the current [lockscreenScene] and
146      * [shadeScene].
147      */
148     class SceneSaver(private val lockscreenScene: SceneKey, private val shadeScene: SceneKey) :
149         Saver<SceneKey, String> {
150         override fun SaverScope.save(value: SceneKey): String = value.debugName
151 
152         override fun restore(value: String): SceneKey {
153             return ensureCorrectScene(AllScenes.getValue(value), lockscreenScene, shadeScene)
154         }
155     }
156 
157     fun ensureCorrectScene(
158         scene: SceneKey,
159         lockscreenScene: SceneKey,
160         shadeScene: SceneKey,
161     ): SceneKey {
162         return when (scene) {
163             Lockscreen,
164             SplitLockscreen -> lockscreenScene
165             Shade,
166             SplitShade -> shadeScene
167             // We should never be in the QuickSettings page if the SplitShade is a possible scene.
168             QuickSettings -> if (shadeScene == SplitShade) SplitShade else QuickSettings
169             else -> scene
170         }
171     }
172 }
173 
174 object Overlays {
175     val Notifications = OverlayKey("NotificationsOverlay")
176     val QuickSettings = OverlayKey("QuickSettingsOverlay")
177 }
178 
179 /** A [Saver] that restores a [MutableSceneTransitionLayoutState] to its previous [currentScene]. */
180 class MutableSceneTransitionLayoutSaver(
181     private val sceneSaver: Scenes.SceneSaver,
182     private val transitions: SceneTransitions,
183     private val canChangeScene: (SceneKey) -> Boolean,
184     private val motionScheme: MotionScheme,
185 ) : Saver<MutableSceneTransitionLayoutState, String> {
savenull186     override fun SaverScope.save(state: MutableSceneTransitionLayoutState): String {
187         val currentScene = state.transitionState.currentScene
188         return with(sceneSaver) { save(currentScene) }
189     }
190 
restorenull191     override fun restore(value: String): MutableSceneTransitionLayoutState {
192         val currentScene = sceneSaver.restore(value)
193         return MutableSceneTransitionLayoutState(
194             initialScene = currentScene,
195             motionScheme = motionScheme,
196             transitions = transitions,
197             canChangeScene = canChangeScene,
198         )
199     }
200 }
201 
202 @Composable
SystemUinull203 fun SystemUi(modifier: Modifier = Modifier) {
204     var configuration by
205         rememberSaveable(stateSaver = DemoConfiguration.Saver) {
206             mutableStateOf(DemoConfiguration())
207         }
208     SystemUi(configuration, { configuration = it }, modifier)
209 }
210 
211 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
212 @Composable
SystemUinull213 fun SystemUi(
214     configuration: DemoConfiguration,
215     onConfigurationChange: (DemoConfiguration) -> Unit,
216     modifier: Modifier = Modifier,
217     initialScene: SceneKey? = null,
218 ) {
219     val windowSizeClass = calculateWindowSizeClass()
220     val shouldUseSplitScenes = shouldUseSplitScenes(windowSizeClass)
221 
222     val lockscreenScene: SceneKey
223     val shadeScene: SceneKey
224     val launcherColumns: Int
225     if (shouldUseSplitScenes) {
226         lockscreenScene = Scenes.SplitLockscreen
227         shadeScene = Scenes.SplitShade
228         launcherColumns = 8
229     } else {
230         lockscreenScene = Scenes.Lockscreen
231         shadeScene = Scenes.Shade
232         launcherColumns = 4
233     }
234 
235     val notificationCountInLockscreen = configuration.notificationsInLockscreen
236     val notificationCount = max(notificationCountInLockscreen, configuration.notificationsInShade)
237     val interactiveNotifications = configuration.interactiveNotifications
238     val notificationTextMeasurer = rememberTextMeasurer(cacheSize = notificationCount * 2)
239     val motionScheme = MaterialTheme.motionScheme
240     val notifications =
241         remember(
242             interactiveNotifications,
243             notificationCount,
244             notificationCountInLockscreen,
245             notificationTextMeasurer,
246             motionScheme,
247         ) {
248             notifications(
249                 interactiveNotifications,
250                 notificationCount,
251                 notificationCountInLockscreen,
252                 notificationTextMeasurer,
253                 motionScheme,
254             )
255         }
256     val expectedQsSize = 12
257     val quickSettingsTextMeasurer = rememberTextMeasurer(cacheSize = expectedQsSize * 2)
258     val quickSettingsTiles = remember { quickSettingsTiles(quickSettingsTextMeasurer) }
259     check(expectedQsSize == quickSettingsTiles.size)
260 
261     var isLockscreenDismissable by remember { mutableStateOf(false) }
262     var isLockscreenDismissed by remember { mutableStateOf(false) }
263     var showConfigurationDialog by remember { mutableStateOf(false) }
264 
265     val nQuickSettingsColumns =
266         if (configuration.enableOverlays) {
267             2
268         } else {
269             when (windowSizeClass.widthSizeClass) {
270                 WindowWidthSizeClass.Compact -> 2
271                 WindowWidthSizeClass.Medium,
272                 WindowWidthSizeClass.Expanded ->
273                     when (windowSizeClass.heightSizeClass) {
274                         // Phone landscape.
275                         WindowHeightSizeClass.Compact -> 2
276                         else -> 3
277                     }
278 
279                 else -> error("Unknown size class: ${windowSizeClass.widthSizeClass}")
280             }
281         }
282     val nQuickSettingsRow = configuration.quickSettingsRows
283     val nQuickSettingsSplitShadeRows = nQuickSettingsColumns
284 
285     // The state of the quick settings pager in the phone (one column) layout.
286     val nQuickSettingsPages =
287         nQuickSettingsPages(
288             nTiles = quickSettingsTiles.size,
289             nRows = nQuickSettingsRow,
290             nColumns = nQuickSettingsColumns,
291         )
292     val quickSettingsPagerState = rememberPagerState { nQuickSettingsPages }
293 
294     val hapticFeedback = LocalHapticFeedback.current
295     val revealHaptics = remember(hapticFeedback) { DemoContainerRevealHaptics(hapticFeedback) }
296     val shadeMotionSpec =
297         remember(shouldUseSplitScenes) {
298             VerticalExpandContainerSpec(isFloating = shouldUseSplitScenes)
299         }
300     val transitions =
301         remember(quickSettingsPagerState, revealHaptics, shouldUseSplitScenes) {
302             systemUiTransitions(quickSettingsPagerState, revealHaptics, shadeMotionSpec)
303         }
304 
305     val sceneSaver =
306         remember(lockscreenScene, shadeScene) { Scenes.SceneSaver(lockscreenScene, shadeScene) }
307 
308     fun maybeUpdateLockscreenDismissed(scene: SceneKey) {
309         when (scene) {
310             Scenes.Launcher -> isLockscreenDismissed = true
311             Scenes.Lockscreen,
312             Scenes.SplitLockscreen -> isLockscreenDismissed = false
313 
314             else -> {}
315         }
316     }
317 
318     val canChangeScene =
319         remember(configuration) {
320             { scene: SceneKey ->
321                 if (configuration.canChangeSceneOrOverlays) {
322                     maybeUpdateLockscreenDismissed(scene)
323                     true
324                 } else {
325                     false
326                 }
327             }
328         }
329 
330     val stateSaver =
331         remember(sceneSaver, transitions, canChangeScene, motionScheme) {
332             MutableSceneTransitionLayoutSaver(
333                 sceneSaver = sceneSaver,
334                 transitions = transitions,
335                 canChangeScene = canChangeScene,
336                 motionScheme = motionScheme,
337             )
338         }
339     val layoutState =
340         rememberSaveable(
341             transitions,
342             canChangeScene,
343             configuration,
344             motionScheme,
345             saver = stateSaver,
346         ) {
347             val initialScene =
348                 initialScene?.let {
349                     Scenes.ensureCorrectScene(
350                         initialScene,
351                         lockscreenScene = lockscreenScene,
352                         shadeScene = shadeScene,
353                     )
354                 } ?: lockscreenScene
355 
356             MutableSceneTransitionLayoutState(
357                 initialScene = initialScene,
358                 motionScheme = motionScheme,
359                 transitions = transitions,
360                 canChangeScene = canChangeScene,
361                 canShowOverlay = { configuration.canChangeSceneOrOverlays },
362                 canHideOverlay = { configuration.canChangeSceneOrOverlays },
363                 canReplaceOverlay = { _, _ -> configuration.canChangeSceneOrOverlays },
364                 deferTransitionProgress = configuration.deferTransitionProgress,
365             )
366         }
367 
368     val coroutineScope = rememberCoroutineScope()
369     fun onChangeScene(scene: SceneKey) {
370         maybeUpdateLockscreenDismissed(scene)
371 
372         // Enforce that we are going to the right shade/lockscreen here depending on the windows
373         // size class.
374         layoutState.setTargetScene(
375             Scenes.ensureCorrectScene(scene, lockscreenScene, shadeScene),
376             coroutineScope,
377         )
378     }
379 
380     fun onPowerButtonClicked() {
381         isLockscreenDismissable = false
382         isLockscreenDismissed = false
383 
384         if (layoutState.transitionState.currentScene == Scenes.AlwaysOnDisplay) {
385             onChangeScene(lockscreenScene)
386         } else {
387             onChangeScene(Scenes.AlwaysOnDisplay)
388         }
389     }
390 
391     fun onSettingsButtonClicked() {
392         showConfigurationDialog = true
393     }
394 
395     fun onExpandButtonClicked() {
396         onConfigurationChange(configuration.copy(isFullscreen = true))
397     }
398 
399     @Composable
400     fun ContentScope.NotificationList(
401         maxNotificationCount: Int,
402         isScrollable: Boolean = true,
403         overscrollEffect: OverscrollEffect? = null,
404     ) {
405         NotificationList(
406             notifications = notifications,
407             maxNotificationCount = maxNotificationCount,
408             demoConfiguration = configuration,
409             isScrollable = isScrollable,
410             overscrollEffect = overscrollEffect,
411         )
412     }
413 
414     if (showConfigurationDialog) {
415         DemoConfigurationDialog(
416             configuration,
417             onConfigurationChange,
418             onDismissRequest = { showConfigurationDialog = false },
419         )
420     }
421 
422     Column(modifier) {
423         if (!configuration.isFullscreen) {
424             Row(Modifier.horizontalScroll(rememberScrollState())) {
425                 IconButton(::onPowerButtonClicked) { Icon(Icons.Default.PowerSettingsNew, null) }
426                 IconButton(::onSettingsButtonClicked) { Icon(Icons.Default.Settings, null) }
427                 IconButton(::onExpandButtonClicked) { Icon(Icons.Default.ZoomOutMap, null) }
428 
429                 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
430                     listOf(
431                             Scenes.AlwaysOnDisplay to "AOD",
432                             Scenes.Lockscreen to "Lock",
433                             Scenes.Bouncer to "Bouncer",
434                             Scenes.Launcher to "Gone",
435                             Scenes.Shade to "Shade",
436                             Scenes.QuickSettings to "QS",
437                         )
438                         .forEach { (scene, name) ->
439                             Button(onClick = { onChangeScene(scene) }) { Text(name) }
440                         }
441 
442                     listOf(Overlays.Notifications to "NS", Overlays.QuickSettings to "QSS")
443                         .forEach { (overlay, name) ->
444                             Button(
445                                 onClick = {
446                                     if (layoutState.currentOverlays.contains(overlay)) {
447                                         layoutState.hideOverlay(overlay, coroutineScope)
448                                     } else {
449                                         layoutState.showOverlay(overlay, coroutineScope)
450                                     }
451                                 }
452                             ) {
453                                 Text(name)
454                             }
455                         }
456                 }
457             }
458         }
459 
460         // Provide an easy way to leave full screen mode by going back.
461         BackHandler(enabled = configuration.isFullscreen) {
462             onConfigurationChange(configuration.copy(isFullscreen = false))
463         }
464 
465         val shape = RoundedCornerShape(Shade.Dimensions.ScrimCornerSize)
466         val borderColor = MaterialTheme.colorScheme.onSurface
467 
468         Surface(
469             Modifier.semantics { testTagsAsResourceId = true }
470                 .thenIf(!configuration.isFullscreen) {
471                     Modifier.padding(3.dp)
472                         .then(
473                             if (configuration.transitionBorder) {
474                                 Modifier.border(
475                                     5.dp,
476                                     if (layoutState.isTransitioning()) Color.Red else Color.Green,
477                                     shape,
478                                 )
479                             } else {
480                                 Modifier.border(1.dp, borderColor, shape)
481                             }
482                         )
483                         .clip(shape)
484                 },
485             color = MaterialTheme.colorScheme.surfaceVariant,
486         ) {
487             val stretchOverscrollFactory = LocalOverscrollFactory.current
488             CompositionLocalProvider(
489                 LocalContentColor provides MaterialTheme.colorScheme.onSurface,
490                 LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory(),
491             ) {
492                 var isMediaPlayerPlaying by remember { mutableStateOf(false) }
493                 val mediaPlayer:
494                     (@Composable
495                     ContentScope.(presentationStyle: DemoMediaPresentationStyle) -> Unit)? =
496                     if (configuration.showMediaPlayer) {
497                         { presentationStyle ->
498                             MediaPlayer(
499                                 presentationStyle = presentationStyle,
500                                 isPlaying = isMediaPlayerPlaying,
501                                 onIsPlayingChange = { isMediaPlayerPlaying = it },
502                                 onVisibilityChange = { isVisible ->
503                                     onConfigurationChange(
504                                         configuration.copy(showMediaPlayer = isVisible)
505                                     )
506                                 },
507                             )
508                         }
509                     } else {
510                         null
511                     }
512                 val defaultMediaPlayer: (@Composable ContentScope.() -> Unit)? =
513                     mediaPlayer?.let { { it(DemoMediaPresentationStyle.Default) } }
514                 val compactMediaPlayer: (@Composable ContentScope.() -> Unit)? =
515                     mediaPlayer?.let { { it(DemoMediaPresentationStyle.Compact) } }
516 
517                 val qsPager: (@Composable ContentScope.() -> Unit) = {
518                     QuickSettingsPager(
519                         pagerState = quickSettingsPagerState,
520                         tiles = quickSettingsTiles,
521                         nRows = nQuickSettingsRow,
522                         nColumns = nQuickSettingsColumns,
523                     )
524                 }
525 
526                 // SceneTransitionLayout can only be bound to one SceneTransitionLayoutState, so
527                 // make sure we recompose it fully when we create a new state object.
528                 key(layoutState) {
529                     val overlayEffectFactory =
530                         if (shouldUseSplitScenes) null else stretchOverscrollFactory
531 
532                     SceneTransitionLayout(
533                         state = layoutState,
534                         transitionInterceptionThreshold =
535                             configuration.transitionInterceptionThreshold,
536                         modifier =
537                             // Make this layout accessible to UiAutomator.
538                             Modifier.thenIf(layoutState.currentTransition == null) {
539                                 Modifier.testTag("SystemUiSceneTransitionLayout:idle")
540                             },
541                         swipeSourceDetector =
542                             if (configuration.enableOverlays) {
543                                 remember { SceneContainerSwipeDetector(edgeSize = 60.dp) }
544                             } else {
545                                 DefaultEdgeDetector
546                             },
547                         implicitTestTags = true,
548                     ) {
549                         scene(Scenes.Launcher, Launcher.userActions(shadeScene, configuration)) {
550                             FirstCompositionDelay(configuration)
551                             Launcher(launcherColumns)
552                         }
553                         scene(
554                             Scenes.Lockscreen,
555                             Lockscreen.userActions(
556                                 isLockscreenDismissable,
557                                 shadeScene,
558                                 requiresFullDistanceSwipeToShade =
559                                     when (configuration.lsToShadeRequiresFullSwipe) {
560                                         ToggleableState.On -> true
561                                         ToggleableState.Off -> false
562                                         ToggleableState.Indeterminate ->
563                                             configuration.interactiveNotifications
564                                     },
565                                 configuration,
566                             ),
567                         ) {
568                             FirstCompositionDelay(configuration)
569                             Lockscreen(
570                                 notificationList = {
571                                     NotificationList(
572                                         maxNotificationCount =
573                                             configuration.notificationsInLockscreen
574                                     )
575                                 },
576                                 mediaPlayer = defaultMediaPlayer,
577                                 isDismissable = isLockscreenDismissable,
578                                 onToggleDismissable = {
579                                     isLockscreenDismissable = !isLockscreenDismissable
580                                 },
581                                 ::onChangeScene,
582                             )
583                         }
584                         scene(
585                             Scenes.SplitLockscreen,
586                             SplitLockscreen.userActions(
587                                 isLockscreenDismissable,
588                                 shadeScene,
589                                 configuration,
590                             ),
591                         ) {
592                             FirstCompositionDelay(configuration)
593                             SplitLockscreen(
594                                 notificationList = {
595                                     NotificationList(
596                                         maxNotificationCount =
597                                             configuration.notificationsInLockscreen
598                                     )
599                                 },
600                                 mediaPlayer = defaultMediaPlayer,
601                                 isDismissable = isLockscreenDismissable,
602                                 onToggleDismissable = {
603                                     isLockscreenDismissable = !isLockscreenDismissable
604                                 },
605                                 ::onChangeScene,
606                                 configuration = configuration,
607                             )
608                         }
609                         scene(Scenes.StubStart, Stub.startUserActions(lockscreenScene)) {
610                             FirstCompositionDelay(configuration)
611                             Stub(
612                                 rootKey = Stub.Elements.SceneStart,
613                                 textKey = Stub.Elements.TextStart,
614                                 text = "Stub scene (start)",
615                             )
616                         }
617                         scene(Scenes.StubEnd, Stub.endUserActions(lockscreenScene)) {
618                             FirstCompositionDelay(configuration)
619                             Stub(
620                                 rootKey = Stub.Elements.SceneEnd,
621                                 textKey = Stub.Elements.TextEnd,
622                                 text = "Stub scene (end)",
623                             )
624                         }
625                         scene(Scenes.Camera, Camera.userActions(lockscreenScene)) {
626                             FirstCompositionDelay(configuration)
627                             Camera()
628                         }
629                         scene(Scenes.Bouncer, Bouncer.userActions(lockscreenScene)) {
630                             FirstCompositionDelay(configuration)
631                             Bouncer(
632                                 onBouncerCancelled = { onChangeScene(lockscreenScene) },
633                                 onBouncerSolved = { onChangeScene(Scenes.Launcher) },
634                             )
635                         }
636                         scene(
637                             Scenes.QuickSettings,
638                             QuickSettings.userActions(
639                                 shadeScene,
640                                 lockscreenScene,
641                                 isLockscreenDismissed,
642                             ),
643                         ) {
644                             FirstCompositionDelay(configuration)
645                             QuickSettings(
646                                 qsPager,
647                                 mediaPlayer = defaultMediaPlayer,
648                                 ::onSettingsButtonClicked,
649                                 ::onPowerButtonClicked,
650                             )
651                         }
652                         scene(
653                             Scenes.Shade,
654                             Shade.userActions(isLockscreenDismissed, lockscreenScene),
655                         ) {
656                             FirstCompositionDelay(configuration)
657                             Shade(
658                                 notificationList = { overscrollEffect ->
659                                     NotificationList(
660                                         maxNotificationCount = configuration.notificationsInShade,
661                                         overscrollEffect = overscrollEffect,
662                                     )
663                                 },
664                                 mediaPlayer = defaultMediaPlayer,
665                                 quickSettingsTiles,
666                                 nQuickSettingsColumns,
667                             )
668                         }
669                         scene(
670                             Scenes.SplitShade,
671                             SplitShade.userActions(isLockscreenDismissed, lockscreenScene),
672                         ) {
673                             FirstCompositionDelay(configuration)
674                             SplitShade(
675                                 notificationList = {
676                                     NotificationList(
677                                         maxNotificationCount = configuration.notificationsInShade
678                                     )
679                                 },
680                                 mediaPlayer = defaultMediaPlayer,
681                                 quickSettingsTiles,
682                                 nQuickSettingsSplitShadeRows,
683                                 nQuickSettingsColumns,
684                                 ::onSettingsButtonClicked,
685                                 ::onPowerButtonClicked,
686                             )
687                         }
688 
689                         scene(Scenes.AlwaysOnDisplay) {
690                             FirstCompositionDelay(configuration)
691                             AlwaysOnDisplay(Modifier.clickable { onChangeScene(lockscreenScene) })
692                         }
693 
694                         overlay(
695                             Overlays.QuickSettings,
696                             userActions = QuickSettingsShade.UserActions,
697                             alignment = Alignment.TopEnd,
698                             effectFactory = overlayEffectFactory,
699                         ) {
700                             FirstCompositionDelay(configuration)
701                             QuickSettingsShade(qsPager, compactMediaPlayer)
702                         }
703 
704                         overlay(
705                             Overlays.Notifications,
706                             userActions = NotificationShade.UserActions,
707                             alignment = Alignment.TopStart,
708                             effectFactory = overlayEffectFactory,
709                         ) {
710                             FirstCompositionDelay(configuration)
711                             NotificationShade(
712                                 clock =
713                                     if (shouldUseSplitScenes) {
714                                         null
715                                     } else {
716                                         { Clock(MaterialTheme.colorScheme.onSurfaceVariant) }
717                                     },
718                                 mediaPlayer = defaultMediaPlayer,
719                                 notificationList = {
720                                     NotificationList(
721                                         maxNotificationCount = configuration.notificationsInShade,
722                                         isScrollable = false,
723                                     )
724                                 },
725                             )
726                         }
727                     }
728 
729                     // Add 2 empty boxes for each half of the STL. This is used by overlay benchmark
730                     // tests to swipe on the start or end half of the STL.
731                     Row {
732                         Box(Modifier.testTag("StlStartHalf").fillMaxHeight().weight(1f))
733                         Box(Modifier.testTag("StlEndHalf").fillMaxHeight().weight(1f))
734                     }
735                 }
736             }
737         }
738     }
739 }
740 
741 @Composable
FirstCompositionDelaynull742 private fun FirstCompositionDelay(configuration: DemoConfiguration) {
743     val delay = configuration.firstCompositionDelay
744     if (delay > 0L) {
745         remember<Any> { Thread.sleep(delay) }
746     }
747 }
748 
749 // Adapted from [androidx.compose.material3.windowsizeclass.calculateWindowSizeClass].
750 @Composable
calculateWindowSizeClassnull751 internal fun calculateWindowSizeClass(): WindowSizeClass {
752     // Observe view configuration changes and recalculate the size class on each change. We can't
753     // use Activity#onConfigurationChanged as this will sometimes fail to be called on different
754     // API levels, hence why this function needs to be @Composable so we can observe the
755     // ComposeView's configuration changes.
756     LocalConfiguration.current
757 
758     return calculateWindowSizeClass(LocalContext.current, LocalDensity.current)
759 }
760 
761 @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
calculateWindowSizeClassnull762 fun calculateWindowSizeClass(context: Context, density: Density): WindowSizeClass {
763     val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
764     val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() }
765     return WindowSizeClass.calculateFromSize(size)
766 }
767 
shouldUseSplitScenesnull768 fun shouldUseSplitScenes(windowSizeClass: WindowSizeClass): Boolean {
769     return when (windowSizeClass.widthSizeClass) {
770         WindowWidthSizeClass.Compact,
771         WindowWidthSizeClass.Medium -> false
772         WindowWidthSizeClass.Expanded -> true
773         else -> error("Unknown size class: ${windowSizeClass.widthSizeClass}")
774     }
775 }
776 
quickSettingsTilesnull777 private fun quickSettingsTiles(textMeasurer: TextMeasurer): List<QuickSettingsTileViewModel> {
778     return listOf(
779         quickSettingsTile(
780             textMeasurer = textMeasurer,
781             icon = Icons.Default.NetworkWifi,
782             title = "Internet",
783             description = "Google Guest",
784             isActive = true,
785             showChevron = true,
786         ),
787         quickSettingsTile(
788             textMeasurer = textMeasurer,
789             icon = Icons.Default.Bluetooth,
790             title = "Bluetooth",
791             isActive = true,
792             description = "On",
793             inactiveDescription = "Off",
794         ),
795         quickSettingsTile(
796             textMeasurer = textMeasurer,
797             icon = Icons.Default.DoNotDisturb,
798             title = "Do Not Disturb",
799             description = "On",
800             inactiveDescription = "Off",
801         ),
802         quickSettingsTile(
803             textMeasurer = textMeasurer,
804             icon = Icons.Default.FlashlightOff,
805             title = "Flashlight",
806             description = "On",
807             inactiveDescription = "Off",
808         ),
809         quickSettingsTile(
810             textMeasurer = textMeasurer,
811             icon = Icons.Default.AirplanemodeInactive,
812             title = "Airplane mode",
813             description = "On",
814             inactiveDescription = "Off",
815         ),
816         quickSettingsTile(
817             textMeasurer = textMeasurer,
818             icon = Icons.Default.Home,
819             title = "Home",
820             description = "1600 Amphitheatre Pkwy",
821             isActive = true,
822             showChevron = true,
823         ),
824         quickSettingsTile(
825             textMeasurer = textMeasurer,
826             icon = Icons.Default.CreditCard,
827             title = "GPay",
828             description = "•••• 0061",
829             isActive = true,
830         ),
831         quickSettingsTile(
832             textMeasurer = textMeasurer,
833             icon = Icons.Default.ScreenRotation,
834             title = "Auto-rotate",
835             isActive = true,
836             description = "On",
837             inactiveDescription = "Off",
838         ),
839         quickSettingsTile(
840             textMeasurer = textMeasurer,
841             icon = Icons.Default.Bedtime,
842             title = "Night Light",
843             description = "On",
844             inactiveDescription = "Off",
845         ),
846         quickSettingsTile(
847             textMeasurer = textMeasurer,
848             icon = Icons.Default.Videocam,
849             title = "Screen record",
850             description = "Start",
851         ),
852         quickSettingsTile(
853             textMeasurer = textMeasurer,
854             icon = Icons.Default.NearbyOff,
855             title = "Nearby Share",
856             showChevron = true,
857         ),
858         quickSettingsTile(
859             textMeasurer = textMeasurer,
860             icon = Icons.Default.ChargingStation,
861             title = "Battery Share",
862             description = "On",
863             inactiveDescription = "Off",
864         ),
865     )
866 }
867 
quickSettingsTilenull868 private fun quickSettingsTile(
869     textMeasurer: TextMeasurer,
870     icon: ImageVector,
871     title: String,
872     key: ElementKey = ElementKey("Tile:$title", identity = QuickSettingsTileIdentity()),
873     isActive: Boolean = false,
874     description: String? = null,
875     inactiveDescription: String? = description,
876     showChevron: Boolean = false,
877 ): QuickSettingsTileViewModel {
878     val activeDescription = description
879 
880     var isActive by mutableStateOf(isActive)
881     var description by mutableStateOf(if (isActive) activeDescription else inactiveDescription)
882 
883     return object : QuickSettingsTileViewModel {
884         override val key: ElementKey = key
885         override val isActive: Boolean
886             get() = isActive
887 
888         override val icon: ImageVector = icon
889         override val title: String = title
890         override val description: String?
891             get() = description
892 
893         override val showChevron: Boolean = showChevron
894         override val onClick: () -> Unit = {
895             isActive = !isActive
896             description =
897                 if (isActive) {
898                     activeDescription
899                 } else {
900                     inactiveDescription
901                 }
902         }
903 
904         override val textMeasurer: TextMeasurer = textMeasurer
905     }
906 }
907