• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.qs.composefragment
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import android.content.res.Configuration
22 import android.graphics.Canvas
23 import android.graphics.Path
24 import android.graphics.PointF
25 import android.graphics.Rect
26 import android.os.Bundle
27 import android.os.Trace
28 import android.util.IndentingPrintWriter
29 import android.view.LayoutInflater
30 import android.view.MotionEvent
31 import android.view.View
32 import android.view.ViewConfiguration
33 import android.view.ViewGroup
34 import android.widget.FrameLayout
35 import androidx.activity.OnBackPressedDispatcher
36 import androidx.activity.OnBackPressedDispatcherOwner
37 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
38 import androidx.annotation.VisibleForTesting
39 import androidx.compose.animation.core.tween
40 import androidx.compose.foundation.ScrollState
41 import androidx.compose.foundation.isSystemInDarkTheme
42 import androidx.compose.foundation.layout.Arrangement.spacedBy
43 import androidx.compose.foundation.layout.Box
44 import androidx.compose.foundation.layout.Column
45 import androidx.compose.foundation.layout.Row
46 import androidx.compose.foundation.layout.Spacer
47 import androidx.compose.foundation.layout.fillMaxSize
48 import androidx.compose.foundation.layout.fillMaxWidth
49 import androidx.compose.foundation.layout.offset
50 import androidx.compose.foundation.layout.padding
51 import androidx.compose.foundation.layout.requiredHeightIn
52 import androidx.compose.foundation.verticalScroll
53 import androidx.compose.material3.MaterialTheme
54 import androidx.compose.runtime.Composable
55 import androidx.compose.runtime.CompositionLocalProvider
56 import androidx.compose.runtime.DisposableEffect
57 import androidx.compose.runtime.LaunchedEffect
58 import androidx.compose.runtime.getValue
59 import androidx.compose.runtime.mutableFloatStateOf
60 import androidx.compose.runtime.mutableStateOf
61 import androidx.compose.runtime.remember
62 import androidx.compose.runtime.setValue
63 import androidx.compose.runtime.snapshotFlow
64 import androidx.compose.ui.Alignment
65 import androidx.compose.ui.Modifier
66 import androidx.compose.ui.graphics.Color
67 import androidx.compose.ui.graphics.graphicsLayer
68 import androidx.compose.ui.input.pointer.PointerEventPass
69 import androidx.compose.ui.input.pointer.PointerInputChange
70 import androidx.compose.ui.input.pointer.pointerInput
71 import androidx.compose.ui.layout.approachLayout
72 import androidx.compose.ui.layout.layout
73 import androidx.compose.ui.layout.onPlaced
74 import androidx.compose.ui.layout.onSizeChanged
75 import androidx.compose.ui.layout.positionInRoot
76 import androidx.compose.ui.layout.positionOnScreen
77 import androidx.compose.ui.platform.ComposeView
78 import androidx.compose.ui.platform.LocalConfiguration
79 import androidx.compose.ui.platform.LocalContext
80 import androidx.compose.ui.res.dimensionResource
81 import androidx.compose.ui.res.stringResource
82 import androidx.compose.ui.semantics.CustomAccessibilityAction
83 import androidx.compose.ui.semantics.customActions
84 import androidx.compose.ui.semantics.semantics
85 import androidx.compose.ui.unit.Dp
86 import androidx.compose.ui.unit.IntOffset
87 import androidx.compose.ui.unit.dp
88 import androidx.compose.ui.unit.round
89 import androidx.compose.ui.util.fastRoundToInt
90 import androidx.compose.ui.viewinterop.AndroidView
91 import androidx.lifecycle.Lifecycle
92 import androidx.lifecycle.compose.collectAsStateWithLifecycle
93 import androidx.lifecycle.lifecycleScope
94 import androidx.lifecycle.repeatOnLifecycle
95 import com.android.compose.animation.scene.ContentKey
96 import com.android.compose.animation.scene.ContentScope
97 import com.android.compose.animation.scene.ElementKey
98 import com.android.compose.animation.scene.ElementMatcher
99 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
100 import com.android.compose.animation.scene.SceneKey
101 import com.android.compose.animation.scene.SceneTransitionLayout
102 import com.android.compose.animation.scene.content.state.TransitionState
103 import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
104 import com.android.compose.animation.scene.transitions
105 import com.android.compose.modifiers.height
106 import com.android.compose.modifiers.padding
107 import com.android.compose.modifiers.thenIf
108 import com.android.compose.theme.PlatformTheme
109 import com.android.mechanics.GestureContext
110 import com.android.systemui.Dumpable
111 import com.android.systemui.Flags
112 import com.android.systemui.Flags.notificationShadeBlur
113 import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
114 import com.android.systemui.brightness.ui.compose.ContainerColors
115 import com.android.systemui.compose.modifiers.sysuiResTag
116 import com.android.systemui.dump.DumpManager
117 import com.android.systemui.keyboard.shortcut.ui.composable.InteractionsConfig
118 import com.android.systemui.keyboard.shortcut.ui.composable.ProvideShortcutHelperIndication
119 import com.android.systemui.lifecycle.repeatWhenAttached
120 import com.android.systemui.lifecycle.setSnapshotBinding
121 import com.android.systemui.media.controls.ui.view.MediaHost
122 import com.android.systemui.plugins.qs.QS
123 import com.android.systemui.plugins.qs.QSContainerController
124 import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
125 import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings
126 import com.android.systemui.qs.composefragment.SceneKeys.debugName
127 import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
128 import com.android.systemui.qs.composefragment.ui.GridAnchor
129 import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
130 import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
131 import com.android.systemui.qs.composefragment.ui.toEditMode
132 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
133 import com.android.systemui.qs.flags.QSComposeFragment
134 import com.android.systemui.qs.footer.ui.compose.FooterActions
135 import com.android.systemui.qs.panels.ui.compose.EditMode
136 import com.android.systemui.qs.panels.ui.compose.QuickQuickSettings
137 import com.android.systemui.qs.panels.ui.compose.TileGrid
138 import com.android.systemui.qs.shared.ui.ElementKeys
139 import com.android.systemui.qs.ui.composable.QuickSettingsShade
140 import com.android.systemui.qs.ui.composable.QuickSettingsShade.systemGestureExclusionInShade
141 import com.android.systemui.qs.ui.composable.QuickSettingsTheme
142 import com.android.systemui.res.R
143 import com.android.systemui.util.LifecycleFragment
144 import com.android.systemui.util.animation.UniqueObjectHostView
145 import com.android.systemui.util.asIndenting
146 import com.android.systemui.util.printSection
147 import com.android.systemui.util.println
148 import java.io.PrintWriter
149 import java.util.function.Consumer
150 import javax.inject.Inject
151 import kotlinx.coroutines.CompletableDeferred
152 import kotlinx.coroutines.awaitCancellation
153 import kotlinx.coroutines.coroutineScope
154 import kotlinx.coroutines.flow.Flow
155 import kotlinx.coroutines.flow.MutableStateFlow
156 import kotlinx.coroutines.flow.collectLatest
157 import kotlinx.coroutines.flow.combine
158 import kotlinx.coroutines.flow.map
159 import kotlinx.coroutines.launch
160 
161 @SuppressLint("ValidFragment")
162 class QSFragmentCompose
163 @Inject
164 constructor(
165     private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory,
166     private val dumpManager: DumpManager,
167 ) : LifecycleFragment(), QS, Dumpable {
168 
169     private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null)
170     private val collapsedMediaVisibilityChangedListener =
171         MutableStateFlow<(Consumer<Boolean>)?>(null)
172     private val heightListener = MutableStateFlow<QS.HeightListener?>(null)
173     private val qsContainerController = MutableStateFlow<QSContainerController?>(null)
174 
175     private lateinit var viewModel: QSFragmentComposeViewModel
176 
177     private val qqsVisible = MutableStateFlow(false)
178     private val qqsPositionOnRoot = Rect()
179     private val composeViewPositionOnScreen = Rect()
180     private val scrollState = ScrollState(0)
181     private val locationTemp = IntArray(2)
182 
183     // Inside object for namespacing
184     private val notificationScrimClippingParams =
185         object {
186             var isEnabled by mutableStateOf(false)
187             var params by mutableStateOf(NotificationScrimClipParams())
188 
189             fun dump(pw: IndentingPrintWriter) {
190                 pw.printSection("NotificationScrimClippingParams") {
191                     pw.println("isEnabled", isEnabled)
192                     pw.println("params", params)
193                 }
194             }
195         }
196 
197     override fun onStart() {
198         super.onStart()
199         registerDumpable()
200     }
201 
202     override fun onCreate(savedInstanceState: Bundle?) {
203         super.onCreate(savedInstanceState)
204 
205         QSComposeFragment.isUnexpectedlyInLegacyMode()
206         viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope)
207 
208         setListenerCollections()
209         lifecycleScope.launch { viewModel.activate() }
210     }
211 
212     override fun onCreateView(
213         inflater: LayoutInflater,
214         container: ViewGroup?,
215         savedInstanceState: Bundle?,
216     ): View {
217         val context = inflater.context
218         val composeView =
219             ComposeView(context).apply {
220                 id = R.id.quick_settings_container
221                 repeatWhenAttached {
222                     repeatOnLifecycle(Lifecycle.State.CREATED) {
223                         setViewTreeOnBackPressedDispatcherOwner(
224                             object : OnBackPressedDispatcherOwner {
225                                 override val onBackPressedDispatcher =
226                                     OnBackPressedDispatcher().apply {
227                                         setOnBackInvokedDispatcher(
228                                             it.viewRootImpl.onBackInvokedDispatcher
229                                         )
230                                     }
231 
232                                 override val lifecycle: Lifecycle =
233                                     this@repeatWhenAttached.lifecycle
234                             }
235                         )
236                         setContent { this@QSFragmentCompose.Content() }
237                     }
238                 }
239             }
240 
241         val frame =
242             FrameLayoutTouchPassthrough(
243                 context,
244                 { notificationScrimClippingParams.isEnabled },
245                 snapshotFlow { notificationScrimClippingParams.params },
246                 // Only allow scrolling when we are fully expanded. That way, we don't intercept
247                 // swipes in lockscreen (when somehow QS is receiving touches).
248                 { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing },
249                 viewModel::emitMotionEventForFalsingSwipeNested,
250             )
251         frame.addView(
252             composeView,
253             FrameLayout.LayoutParams.MATCH_PARENT,
254             FrameLayout.LayoutParams.MATCH_PARENT,
255         )
256         return frame
257     }
258 
259     @Composable
260     private fun Content() {
261         PlatformTheme(isDarkTheme = if (notificationShadeBlur()) isSystemInDarkTheme() else true) {
262             ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) {
263                 // TODO(b/389985793): Make sure that there is no coroutine work or recompositions
264                 // happening when alwaysCompose is true but isQsVisibleAndAnyShadeExpanded is false.
265                 if (alwaysCompose || viewModel.isQsVisibleAndAnyShadeExpanded) {
266                     Box(
267                         modifier =
268                             Modifier.thenIf(alwaysCompose) {
269                                     Modifier.layout { measurable, constraints ->
270                                         measurable.measure(constraints).run {
271                                             layout(width, height) {
272                                                 if (viewModel.isQsVisibleAndAnyShadeExpanded) {
273                                                     place(0, 0)
274                                                 }
275                                             }
276                                         }
277                                     }
278                                 }
279                                 .graphicsLayer { alpha = viewModel.viewAlpha }
280                                 .thenIf(!Flags.notificationShadeBlur()) {
281                                     Modifier.offset {
282                                         IntOffset(
283                                             x = 0,
284                                             y = viewModel.viewTranslationY.fastRoundToInt(),
285                                         )
286                                     }
287                                 }
288                                 // Disable touches in the whole composable while the mirror is
289                                 // showing. While the mirror is showing, an ancestor of the
290                                 // ComposeView is made alpha 0, but touches are still being captured
291                                 // by the composables.
292                                 .gesturesDisabled(viewModel.showingMirror)
293                     ) {
294                         CollapsableQuickSettingsSTL()
295                     }
296                 }
297             }
298         }
299     }
300 
301     /**
302      * STL that contains both QQS (tiles) and QS (brightness, tiles, footer actions), but no Edit
303      * mode. It tracks [QSFragmentComposeViewModel.expansionState] to drive the transition between
304      * [SceneKeys.QuickQuickSettings] and [SceneKeys.QuickSettings].
305      */
306     @Composable
307     private fun CollapsableQuickSettingsSTL() {
308         val nextCookie = remember {
309             object {
310                 var value = 0
311             }
312         }
313         val transitionToCookie = remember { mutableMapOf<TransitionState.Transition, Int>() }
314         val sceneState =
315             rememberMutableSceneTransitionLayoutState(
316                 initialScene = remember { viewModel.expansionState.toIdleSceneKey() },
317                 transitions =
318                     transitions {
319                         from(QuickQuickSettings, QuickSettings) {
320                             quickQuickSettingsToQuickSettings(viewModel::animateTilesExpansion::get)
321                         }
322                         to(SceneKeys.EditMode) {
323                             spec = tween(durationMillis = EDIT_MODE_TIME_MILLIS)
324                             toEditMode()
325                         }
326                     },
327                 onTransitionStart = { transition ->
328                     val cookie = nextCookie.value++
329                     transitionToCookie[transition] = cookie
330                     Trace.beginAsyncSection(
331                         "CollapsableQuickSettingsSTL ${transition.debugName}",
332                         cookie,
333                     )
334                 },
335                 onTransitionEnd = { transition ->
336                     Trace.endAsyncSection(
337                         "CollapsableQuickSettingsSTL ${transition.debugName}",
338                         transitionToCookie.remove(transition) ?: -1,
339                     )
340                 },
341             )
342 
343         LaunchedEffect(Unit) {
344             synchronizeQsState(
345                 sceneState,
346                 viewModel.containerViewModel.editModeViewModel.isEditing,
347                 snapshotFlow { viewModel.expansionState }.map { it.progress },
348             )
349         }
350 
351         SceneTransitionLayout(state = sceneState, modifier = Modifier.fillMaxSize()) {
352             scene(QuickSettings, alwaysCompose = alwaysCompose) {
353                 LaunchedEffect(Unit) { viewModel.onQSOpen() }
354                 Element(QuickSettings.rootElementKey, Modifier) { QuickSettingsElement() }
355             }
356 
357             scene(QuickQuickSettings, alwaysCompose = alwaysCompose) {
358                 LaunchedEffect(Unit) { viewModel.onQQSOpen() }
359                 // Cannot pass the element modifier in because the top element has a `testTag`
360                 // and this would overwrite it.
361                 Element(QuickQuickSettings.rootElementKey, Modifier) { QuickQuickSettingsElement() }
362             }
363 
364             scene(SceneKeys.EditMode) {
365                 Element(SceneKeys.EditMode.rootElementKey, Modifier) { EditModeElement() }
366             }
367         }
368     }
369 
370     override fun setPanelView(notificationPanelView: QS.HeightListener?) {
371         heightListener.value = notificationPanelView
372     }
373 
374     override fun hideImmediately() {
375         //        view?.animate()?.cancel()
376         //        view?.y = -qsMinExpansionHeight.toFloat()
377     }
378 
379     override fun getQsMinExpansionHeight(): Int {
380         return if (viewModel.isInSplitShade) {
381             getQsMinExpansionHeightForSplitShade()
382         } else {
383             viewModel.qqsHeight
384         }
385     }
386 
387     /**
388      * Returns the min expansion height for split shade.
389      *
390      * On split shade, QS is always expanded and goes from the top of the screen to the bottom of
391      * the QS container.
392      */
393     private fun getQsMinExpansionHeightForSplitShade(): Int {
394         view?.getLocationOnScreen(locationTemp)
395         val top = locationTemp.get(1)
396         // We want to get the original top position, so we subtract any translation currently set.
397         val originalTop = (top - (view?.translationY ?: 0f)).toInt()
398         // On split shade the QS view doesn't start at the top of the screen, so we need to add the
399         // top margin.
400         return originalTop + (view?.height ?: 0)
401     }
402 
403     override fun getDesiredHeight(): Int {
404         /*
405          * Looking at the code, it seems that
406          * * If customizing, then the height is that of the view post-layout, which is set by
407          *   QSContainerImpl.calculateContainerHeight, which is the height the customizer takes
408          * * If not customizing, it's the measured height. So we may want to surface that.
409          */
410         return view?.height ?: 0
411     }
412 
413     override fun setHeightOverride(desiredHeight: Int) {
414         viewModel.heightOverride = desiredHeight
415     }
416 
417     override fun setHeaderClickable(qsExpansionEnabled: Boolean) {
418         // Empty method
419     }
420 
421     override fun isCustomizing(): Boolean {
422         return viewModel.isEditing
423     }
424 
425     override fun closeCustomizer() {
426         viewModel.containerViewModel.editModeViewModel.stopEditing()
427     }
428 
429     override fun setOverscrolling(overscrolling: Boolean) {
430         viewModel.isStackScrollerOverscrolling = overscrolling
431     }
432 
433     override fun setExpanded(qsExpanded: Boolean) {
434         viewModel.isQsExpanded = qsExpanded
435     }
436 
437     override fun setListening(listening: Boolean) {
438         // Not needed, views start listening and collection when composed
439     }
440 
441     override fun setQsVisible(qsVisible: Boolean) {
442         viewModel.isQsVisible = qsVisible
443     }
444 
445     override fun isShowingDetail(): Boolean {
446         return isCustomizing
447     }
448 
449     override fun closeDetail() {
450         closeCustomizer()
451     }
452 
453     override fun animateHeaderSlidingOut() {
454         // TODO(b/353254353)
455     }
456 
457     override fun setQsExpansion(
458         qsExpansionFraction: Float,
459         panelExpansionFraction: Float,
460         headerTranslation: Float,
461         squishinessFraction: Float,
462     ) {
463         viewModel.setQsExpansionValue(qsExpansionFraction)
464         viewModel.panelExpansionFraction = panelExpansionFraction
465         viewModel.squishinessFraction = squishinessFraction
466         viewModel.proposedTranslation = headerTranslation
467     }
468 
469     override fun setHeaderListening(listening: Boolean) {
470         // Not needed, header will start listening as soon as it's composed
471     }
472 
473     override fun notifyCustomizeChanged() {
474         // Not needed, only called from inside customizer
475     }
476 
477     override fun setContainerController(controller: QSContainerController?) {
478         qsContainerController.value = controller
479     }
480 
481     override fun setCollapseExpandAction(action: Runnable?) {
482         viewModel.collapseExpandAccessibilityAction = action
483     }
484 
485     override fun getHeightDiff(): Int {
486         return viewModel.heightDiff
487     }
488 
489     override fun getHeader(): View? {
490         QSComposeFragment.isUnexpectedlyInLegacyMode()
491         return null
492     }
493 
494     override fun setShouldUpdateSquishinessOnMedia(shouldUpdate: Boolean) {
495         viewModel.shouldUpdateSquishinessOnMedia = shouldUpdate
496     }
497 
498     override fun setInSplitShade(isInSplitShade: Boolean) {
499         viewModel.isInSplitShade = isInSplitShade
500     }
501 
502     override fun setTransitionToFullShadeProgress(
503         isTransitioningToFullShade: Boolean,
504         qsTransitionFraction: Float,
505         qsSquishinessFraction: Float,
506     ) {
507         viewModel.isTransitioningToFullShade = isTransitioningToFullShade
508         viewModel.lockscreenToShadeProgress = qsTransitionFraction
509         if (isTransitioningToFullShade) {
510             viewModel.squishinessFraction = qsSquishinessFraction
511         }
512     }
513 
514     override fun setFancyClipping(
515         leftInset: Int,
516         top: Int,
517         rightInset: Int,
518         bottom: Int,
519         cornerRadius: Int,
520         visible: Boolean,
521         fullWidth: Boolean,
522     ) {
523         notificationScrimClippingParams.isEnabled = visible
524         notificationScrimClippingParams.params =
525             NotificationScrimClipParams(
526                 top,
527                 bottom,
528                 if (fullWidth) 0 else leftInset,
529                 if (fullWidth) 0 else rightInset,
530                 cornerRadius,
531             )
532     }
533 
534     override fun isFullyCollapsed(): Boolean {
535         return viewModel.isQsFullyCollapsed
536     }
537 
538     override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) {
539         collapsedMediaVisibilityChangedListener.value = listener
540     }
541 
542     override fun setScrollListener(scrollListener: QS.ScrollListener?) {
543         this.scrollListener.value = scrollListener
544     }
545 
546     override fun setOverScrollAmount(overScrollAmount: Int) {
547         viewModel.overScrollAmount = overScrollAmount
548     }
549 
550     override fun setIsNotificationPanelFullWidth(isFullWidth: Boolean) {
551         viewModel.isSmallScreen = isFullWidth
552     }
553 
554     override fun getHeaderTop(): Int {
555         return qqsPositionOnRoot.top
556     }
557 
558     override fun getHeaderBottom(): Int {
559         return qqsPositionOnRoot.bottom
560     }
561 
562     override fun getHeaderLeft(): Int {
563         return qqsPositionOnRoot.left
564     }
565 
566     override fun getHeaderBoundsOnScreen(outBounds: Rect) {
567         outBounds.set(qqsPositionOnRoot)
568         view?.getBoundsOnScreen(composeViewPositionOnScreen)
569             ?: run { composeViewPositionOnScreen.setEmpty() }
570         outBounds.offset(composeViewPositionOnScreen.left, composeViewPositionOnScreen.top)
571     }
572 
573     override fun isHeaderShown(): Boolean {
574         return qqsVisible.value
575     }
576 
577     private fun setListenerCollections() {
578         lifecycleScope.launch {
579             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
580                 this@QSFragmentCompose.view?.setSnapshotBinding {
581                     scrollListener.value?.onQsPanelScrollChanged(scrollState.value)
582                     collapsedMediaVisibilityChangedListener.value?.accept(viewModel.qqsMediaVisible)
583                 }
584                 launch {
585                     setListenerJob(
586                         heightListener,
587                         viewModel.containerViewModel.editModeViewModel.isEditing,
588                     ) {
589                         onQsHeightChanged()
590                     }
591                 }
592                 launch {
593                     setListenerJob(
594                         qsContainerController,
595                         viewModel.containerViewModel.editModeViewModel.isEditing,
596                     ) {
597                         setCustomizerShowing(it, EDIT_MODE_TIME_MILLIS.toLong())
598                     }
599                 }
600             }
601         }
602     }
603 
604     @Composable
605     private fun ContentScope.QuickQuickSettingsElement(modifier: Modifier = Modifier) {
606         val qqsPadding = viewModel.qqsHeaderHeight
607         val bottomPadding = viewModel.qqsBottomPadding
608         DisposableEffect(Unit) {
609             qqsVisible.value = true
610 
611             onDispose { qqsVisible.value = false }
612         }
613         val squishiness by
614             viewModel.quickQuickSettingsViewModel.squishinessViewModel.squishiness
615                 .collectAsStateWithLifecycle()
616 
617         Column(modifier = modifier.sysuiResTag(ResIdTags.quickQsPanel)) {
618             Box(
619                 modifier =
620                     Modifier.fillMaxWidth()
621                         .onPlaced { coordinates ->
622                             val (leftFromRoot, topFromRoot) = coordinates.positionInRoot().round()
623                             qqsPositionOnRoot.set(
624                                 leftFromRoot,
625                                 topFromRoot,
626                                 leftFromRoot + coordinates.size.width,
627                                 topFromRoot + coordinates.size.height,
628                             )
629                             if (squishiness == 1f) {
630                                 viewModel.qqsHeight = coordinates.size.height
631                             }
632                         }
633                         // Use an approach layout to determien the height without squishiness, as
634                         // that's the value that NPVC and QuickSettingsController care about
635                         // (measured height).
636                         .approachLayout(isMeasurementApproachInProgress = { squishiness < 1f }) {
637                             measurable,
638                             constraints ->
639                             viewModel.qqsHeight = lookaheadSize.height
640                             val placeable = measurable.measure(constraints)
641                             layout(placeable.width, placeable.height) { placeable.place(0, 0) }
642                         }
643                         .padding(top = { qqsPadding }, bottom = { bottomPadding })
644             ) {
645                 val Tiles =
646                     @Composable {
647                         QuickQuickSettings(
648                             viewModel = viewModel.quickQuickSettingsViewModel,
649                             listening = {
650                                 /*
651                                  *  When always compose is false, this will always be true, and we'll be
652                                  *  listening whenever this is composed.
653                                  *  When always compose is true, we listen if we are visible and not
654                                  *  fully expanded
655                                  */
656                                 !alwaysCompose ||
657                                     (viewModel.isQsVisibleAndAnyShadeExpanded &&
658                                         viewModel.expansionState.progress < 1f &&
659                                         !viewModel.isEditing)
660                             },
661                         )
662                     }
663                 val Media =
664                     @Composable {
665                         if (viewModel.qqsMediaVisible) {
666                             MediaObject(
667                                 // In order to have stable constraints passed to the AndroidView
668                                 // during expansion (available height changing due to squishiness),
669                                 // We always allow the media here to be as tall as it wants.
670                                 // (b/383085298)
671                                 modifier = Modifier.requiredHeightIn(max = Dp.Infinity),
672                                 mediaHost = viewModel.qqsMediaHost,
673                             )
674                         }
675                     }
676 
677                 if (viewModel.isQsEnabled) {
678                     Box(
679                         modifier =
680                             Modifier.collapseExpandSemanticAction(
681                                     stringResource(
682                                         id = R.string.accessibility_quick_settings_expand
683                                     )
684                                 )
685                                 .padding(horizontal = qsHorizontalMargin())
686                     ) {
687                         QuickQuickSettingsLayout(
688                             tiles = Tiles,
689                             media = Media,
690                             mediaInRow = viewModel.qqsMediaInRow,
691                         )
692                     }
693                 }
694             }
695             Spacer(modifier = Modifier.weight(1f))
696         }
697     }
698 
699     @Composable
700     private fun ContentScope.QuickSettingsElement(modifier: Modifier = Modifier) {
701         val qqsPadding = viewModel.qqsHeaderHeight
702         val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top)
703         Column(
704             modifier =
705                 modifier.collapseExpandSemanticAction(
706                     stringResource(id = R.string.accessibility_quick_settings_collapse)
707                 )
708         ) {
709             if (viewModel.isQsEnabled) {
710                 Element(ElementKeys.QuickSettingsContent, modifier = Modifier.weight(1f)) {
711                     DisposableEffect(Unit) {
712                         lifecycleScope.launch { scrollState.scrollTo(0) }
713                         onDispose { lifecycleScope.launch { scrollState.scrollTo(0) } }
714                     }
715 
716                     Column(
717                         modifier =
718                             Modifier.fillMaxSize()
719                                 .onPlaced { coordinates ->
720                                     val positionOnScreen = coordinates.positionOnScreen()
721                                     val left = positionOnScreen.x
722                                     val right = left + coordinates.size.width
723                                     val top = positionOnScreen.y
724                                     val bottom = top + coordinates.size.height
725                                     viewModel.applyNewQsScrollerBounds(
726                                         left = left,
727                                         top = top,
728                                         right = right,
729                                         bottom = bottom,
730                                     )
731                                 }
732                                 .offset {
733                                     IntOffset(
734                                         x = 0,
735                                         y = viewModel.qsScrollTranslationY.fastRoundToInt(),
736                                     )
737                                 }
738                                 .onSizeChanged { viewModel.qsScrollHeight = it.height }
739                                 .verticalScroll(scrollState)
740                                 .sysuiResTag(ResIdTags.qsScroll)
741                     ) {
742                         val containerViewModel = viewModel.containerViewModel
743                         Spacer(
744                             modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }
745                         )
746                         val BrightnessSlider =
747                             @Composable {
748                                 Box(
749                                     Modifier.systemGestureExclusionInShade(
750                                         enabled = {
751                                             /*
752                                              * While we are transitioning into QS (either from QQS
753                                              * or from gone), the global position of the brightness
754                                              * slider will change in every frame. This causes
755                                              * the modifier to send a new gesture exclusion
756                                              * rectangle on every frame. Instead, only apply the
757                                              * modifier when this is settled.
758                                              */
759                                             layoutState.transitionState is TransitionState.Idle &&
760                                                 viewModel.isNotTransitioning
761                                         }
762                                     )
763                                 ) {
764                                     AlwaysDarkMode {
765                                         BrightnessSliderContainer(
766                                             viewModel =
767                                                 containerViewModel.brightnessSliderViewModel,
768                                             containerColors =
769                                                 ContainerColors(
770                                                     Color.Transparent,
771                                                     ContainerColors.defaultContainerColor,
772                                                 ),
773                                             modifier = Modifier.fillMaxWidth(),
774                                         )
775                                     }
776                                 }
777                             }
778                         val TileGrid =
779                             @Composable {
780                                 Box {
781                                     GridAnchor()
782                                     TileGrid(
783                                         viewModel = containerViewModel.tileGridViewModel,
784                                         modifier = Modifier.fillMaxWidth(),
785                                         listening = {
786                                             /*
787                                              *  When always compose is false, this will always be true,
788                                              *  and we'll be listening whenever this is composed.
789                                              *  When always compose is true, we look a the second
790                                              *  condition and we'll listen if QS is visible AND we are
791                                              *  not fully collapsed.
792                                              */
793                                             !alwaysCompose ||
794                                                 (viewModel.isQsVisibleAndAnyShadeExpanded &&
795                                                     viewModel.expansionState.progress > 0f &&
796                                                     !viewModel.isEditing)
797                                         },
798                                     )
799                                 }
800                             }
801                         val Media =
802                             @Composable {
803                                 if (viewModel.qsMediaVisible) {
804                                     MediaObject(
805                                         mediaHost = viewModel.qsMediaHost,
806                                         update = { translationY = viewModel.qsMediaTranslationY },
807                                     )
808                                 }
809                             }
810                         Box(
811                             modifier =
812                                 Modifier.fillMaxWidth()
813                                     .sysuiResTag(ResIdTags.quickSettingsPanel)
814                                     .padding(
815                                         top = QuickSettingsShade.Dimensions.Padding,
816                                         start = qsHorizontalMargin(),
817                                         end = qsHorizontalMargin(),
818                                     )
819                         ) {
820                             QuickSettingsLayout(
821                                 brightness = BrightnessSlider,
822                                 tiles = TileGrid,
823                                 media = Media,
824                                 mediaInRow = viewModel.qsMediaInRow,
825                             )
826                         }
827                     }
828                 }
829                 QuickSettingsTheme {
830                     Element(
831                         ElementKeys.FooterActions,
832                         Modifier.sysuiResTag(ResIdTags.qsFooterActions),
833                     ) {
834                         FooterActions(
835                             viewModel = viewModel.footerActionsViewModel,
836                             qsVisibilityLifecycleOwner = this@QSFragmentCompose,
837                         )
838                     }
839                 }
840             }
841         }
842     }
843 
844     @Composable
845     private fun EditModeElement(modifier: Modifier = Modifier) {
846         // No need for top padding, the Scaffold inside takes care of the correct insets
847         EditMode(
848             viewModel = viewModel.containerViewModel.editModeViewModel,
849             modifier =
850                 modifier
851                     .fillMaxWidth()
852                     .padding(horizontal = { QuickSettingsShade.Dimensions.Padding.roundToPx() }),
853         )
854     }
855 
856     private fun Modifier.collapseExpandSemanticAction(label: String): Modifier {
857         return viewModel.collapseExpandAccessibilityAction?.let {
858             semantics {
859                 customActions =
860                     listOf(
861                         CustomAccessibilityAction(label) {
862                             it.run()
863                             true
864                         }
865                     )
866             }
867         } ?: this
868     }
869 
870     private fun registerDumpable() {
871         val instanceId = instanceProvider.getNextId()
872         // Add an instanceId because the system may have more than 1 of these when re-inflating and
873         // DumpManager doesn't like repeated identifiers. Also, put it first because DumpHandler
874         // matches by end.
875         val stringId = "$instanceId-QSFragmentCompose"
876         lifecycleScope.launch {
877             lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
878                 try {
879                     dumpManager.registerNormalDumpable(stringId, this@QSFragmentCompose)
880                     awaitCancellation()
881                 } finally {
882                     dumpManager.unregisterDumpable(stringId)
883                 }
884             }
885         }
886     }
887 
888     override fun dump(pw: PrintWriter, args: Array<out String>) {
889         pw.asIndenting().run {
890             notificationScrimClippingParams.dump(this)
891             printSection("QQS positioning") {
892                 println("qqsHeight", "${headerHeight}px")
893                 println("qqsTop", "${headerTop}px")
894                 println("qqsBottom", "${headerBottom}px")
895                 println("qqsLeft", "${headerLeft}px")
896                 println("qqsPositionOnRoot", qqsPositionOnRoot)
897                 val rect = Rect()
898                 getHeaderBoundsOnScreen(rect)
899                 println("qqsPositionOnScreen", rect)
900             }
901             println("QQS visible", qqsVisible.value)
902             println("Always composed", alwaysCompose)
903             if (::viewModel.isInitialized) {
904                 printSection("View Model") { viewModel.dump(this@run, args) }
905             }
906         }
907     }
908 }
909 
setListenerJobnull910 private suspend inline fun <Listener : Any, Data> setListenerJob(
911     listenerFlow: MutableStateFlow<Listener?>,
912     dataFlow: Flow<Data>,
913     crossinline onCollect: suspend Listener.(Data) -> Unit,
914 ) {
915     coroutineScope {
916         try {
917             listenerFlow.collectLatest { listenerOrNull ->
918                 listenerOrNull?.let { currentListener ->
919                     launch {
920                         // Called when editing mode changes
921                         dataFlow.collect { currentListener.onCollect(it) }
922                     }
923                 }
924             }
925             awaitCancellation()
926         } finally {
927             listenerFlow.value = null
928         }
929     }
930 }
931 
932 private val instanceProvider =
933     object {
934         private var currentId = 0
935 
getNextIdnull936         fun getNextId(): Int {
937             return currentId++
938         }
939     }
940 
941 object SceneKeys {
942     val QuickQuickSettings = SceneKey("QuickQuickSettingsScene")
943     val QuickSettings = SceneKey("QuickSettingsScene")
944     val EditMode = SceneKey("EditModeScene")
945 
946     val TransitionState.Transition.debugName: String
947         get() = "[from=${fromContent.debugName}, to=${toContent.debugName}]"
948 
QSFragmentComposeViewModelnull949     fun QSFragmentComposeViewModel.QSExpansionState.toIdleSceneKey(): SceneKey {
950         return when {
951             progress < 0.5f -> QuickQuickSettings
952             else -> QuickSettings
953         }
954     }
955 
956     val QqsTileElementMatcher =
957         object : ElementMatcher {
matchesnull958             override fun matches(key: ElementKey, content: ContentKey): Boolean {
959                 return content == SceneKeys.QuickQuickSettings &&
960                     ElementKeys.TileElementMatcher.matches(key, content)
961             }
962         }
963 }
964 
synchronizeQsStatenull965 private suspend fun synchronizeQsState(
966     state: MutableSceneTransitionLayoutState,
967     editMode: Flow<Boolean>,
968     expansion: Flow<Float>,
969 ) {
970     coroutineScope {
971         val animationScope = this
972 
973         var currentTransition: ExpansionTransition? = null
974 
975         fun snapTo(scene: SceneKey) {
976             state.snapTo(scene)
977             currentTransition = null
978         }
979 
980         editMode.combine(expansion, ::Pair).collectLatest { (editMode, progress) ->
981             if (editMode && state.currentScene != SceneKeys.EditMode) {
982                 state.setTargetScene(SceneKeys.EditMode, animationScope)?.second?.join()
983             } else if (!editMode && state.currentScene == SceneKeys.EditMode) {
984                 state.setTargetScene(SceneKeys.QuickSettings, animationScope)?.second?.join()
985             }
986             if (!editMode) {
987                 when (progress) {
988                     0f -> snapTo(QuickQuickSettings)
989                     1f -> snapTo(QuickSettings)
990                     else -> {
991                         val transition = currentTransition
992                         if (transition != null) {
993                             transition.progress = progress
994                             return@collectLatest
995                         }
996 
997                         val newTransition =
998                             ExpansionTransition(progress).also { currentTransition = it }
999                         state.startTransitionImmediately(
1000                             animationScope = animationScope,
1001                             transition = newTransition,
1002                         )
1003                     }
1004                 }
1005             }
1006         }
1007     }
1008 }
1009 
1010 private class ExpansionTransition(currentProgress: Float) :
1011     TransitionState.Transition.ChangeScene(
1012         fromScene = QuickQuickSettings,
1013         toScene = QuickSettings,
1014     ) {
1015     override val currentScene: SceneKey
1016         get() {
1017             // This should return the logical scene. If the QS STLState is only driven by
1018             // synchronizeQSState() then it probably does not matter which one we return, this is
1019             // only used to compute the current user actions of a STL.
1020             return QuickQuickSettings
1021         }
1022 
1023     override var progress: Float by mutableFloatStateOf(currentProgress)
1024 
1025     override val progressVelocity: Float
1026         get() = 0f
1027 
1028     override val isInitiatedByUserInput: Boolean
1029         get() = true
1030 
1031     override val isUserInputOngoing: Boolean
1032         get() = true
1033 
1034     override val gestureContext: GestureContext? = null
1035 
1036     private val finishCompletable = CompletableDeferred<Unit>()
1037 
runnull1038     override suspend fun run() {
1039         // This transition runs until it is interrupted by another one.
1040         finishCompletable.await()
1041     }
1042 
freezeAndAnimateToCurrentStatenull1043     override fun freezeAndAnimateToCurrentState() {
1044         finishCompletable.complete(Unit)
1045     }
1046 }
1047 
1048 private const val EDIT_MODE_TIME_MILLIS = 500
1049 
1050 /**
1051  * Performs different touch handling based on the state of the ComposeView:
1052  * * Ignore touches below the value returned by [clippingTopProvider], when clipping is enabled, as
1053  *   per [clippingEnabledProvider].
1054  * * Intercept touches that would overscroll QS forward and instead allow them to be used to close
1055  *   the shade.
1056  */
1057 private class FrameLayoutTouchPassthrough(
1058     context: Context,
1059     private val clippingEnabledProvider: () -> Boolean,
1060     private val clippingParams: Flow<NotificationScrimClipParams>,
1061     private val canScrollForwardQs: () -> Boolean,
1062     private val emitMotionEventForFalsing: () -> Unit,
1063 ) : FrameLayout(context) {
1064 
1065     init {
<lambda>null1066         repeatWhenAttached {
1067             repeatOnLifecycle(Lifecycle.State.STARTED) {
1068                 clippingParams.collect { currentClipParams = it }
1069             }
1070         }
1071     }
1072 
1073     private val currentClippingPath = Path()
1074     private var lastWidth = -1
1075         set(value) {
1076             if (field != value) {
1077                 field = value
1078                 updateClippingPath()
1079             }
1080         }
1081 
1082     private var currentClipParams = NotificationScrimClipParams()
1083         set(value) {
1084             if (field != value) {
1085                 field = value
1086                 updateClippingPath()
1087             }
1088         }
1089 
updateClippingPathnull1090     private fun updateClippingPath() {
1091         currentClippingPath.rewind()
1092         if (clippingEnabledProvider()) {
1093             val right = width + currentClipParams.rightInset
1094             val left = -currentClipParams.leftInset
1095             val top = currentClipParams.top
1096             val bottom = currentClipParams.bottom
1097             currentClippingPath.addRoundRect(
1098                 left.toFloat(),
1099                 top.toFloat(),
1100                 right.toFloat(),
1101                 bottom.toFloat(),
1102                 currentClipParams.radius.toFloat(),
1103                 currentClipParams.radius.toFloat(),
1104                 Path.Direction.CW,
1105             )
1106         }
1107         invalidate()
1108     }
1109 
onLayoutnull1110     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
1111         super.onLayout(changed, left, top, right, bottom)
1112         lastWidth = right - left
1113     }
1114 
dispatchDrawnull1115     override fun dispatchDraw(canvas: Canvas) {
1116         if (!currentClippingPath.isEmpty) {
1117             canvas.clipOutPath(currentClippingPath)
1118         }
1119         super.dispatchDraw(canvas)
1120     }
1121 
isTransformedTouchPointInViewnull1122     override fun isTransformedTouchPointInView(
1123         x: Float,
1124         y: Float,
1125         child: View?,
1126         outLocalPoint: PointF?,
1127     ): Boolean {
1128         return if (clippingEnabledProvider() && y + translationY > currentClipParams.top) {
1129             false
1130         } else {
1131             super.isTransformedTouchPointInView(x, y, child, outLocalPoint)
1132         }
1133     }
1134 
1135     val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
1136     var downY = 0f
1137     var preventingIntercept = false
1138 
onTouchEventnull1139     override fun onTouchEvent(event: MotionEvent): Boolean {
1140         val action = event.actionMasked
1141         when (action) {
1142             MotionEvent.ACTION_DOWN -> {
1143                 preventingIntercept = false
1144                 if (canScrollVertically(1)) {
1145                     // If we can scroll down, make sure we're not intercepted by the parent
1146                     preventingIntercept = true
1147                     parent?.requestDisallowInterceptTouchEvent(true)
1148                 } else if (!canScrollVertically(-1)) {
1149                     // Don't pass on the touch to the view, because scrolling will unconditionally
1150                     // disallow interception even if we can't scroll.
1151                     // if a user can't scroll at all, we should never listen to the touch.
1152                     return false
1153                 }
1154             }
1155             MotionEvent.ACTION_UP -> {
1156                 if (preventingIntercept) {
1157                     emitMotionEventForFalsing()
1158                 }
1159             }
1160         }
1161         return super.onTouchEvent(event)
1162     }
1163 
onInterceptTouchEventnull1164     override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
1165         // If there's a touch on this view and we can scroll down, we don't want to be intercepted
1166         val action = ev.actionMasked
1167 
1168         when (action) {
1169             MotionEvent.ACTION_DOWN -> {
1170                 preventingIntercept = false
1171                 // If we can scroll down, make sure none of our parents intercepts us.
1172                 if (canScrollForwardQs()) {
1173                     preventingIntercept = true
1174                     parent?.requestDisallowInterceptTouchEvent(true)
1175                 }
1176                 downY = ev.y
1177             }
1178 
1179             MotionEvent.ACTION_MOVE -> {
1180                 val y = ev.y.toInt()
1181                 val yDiff: Float = y - downY
1182                 if (yDiff < -touchSlop && !canScrollForwardQs()) {
1183                     // Intercept touches that are overscrolling.
1184                     return true
1185                 }
1186             }
1187         }
1188         return super.onInterceptTouchEvent(ev)
1189     }
1190 }
1191 
gesturesDisablednull1192 private fun Modifier.gesturesDisabled(disabled: Boolean) =
1193     if (disabled) {
1194         pointerInput(Unit) {
1195             awaitPointerEventScope {
1196                 // we should wait for all new pointer events
1197                 while (true) {
1198                     awaitPointerEvent(pass = PointerEventPass.Initial)
1199                         .changes
1200                         .forEach(PointerInputChange::consume)
1201                 }
1202             }
1203         }
1204     } else {
1205         this
1206     }
1207 
1208 @Composable
MediaObjectnull1209 private fun MediaObject(
1210     mediaHost: MediaHost,
1211     modifier: Modifier = Modifier,
1212     update: UniqueObjectHostView.() -> Unit = {},
1213 ) {
<lambda>null1214     Box {
1215         AndroidView(
1216             modifier = modifier,
1217             factory = {
1218                 mediaHost.hostView.apply {
1219                     layoutParams =
1220                         FrameLayout.LayoutParams(
1221                             FrameLayout.LayoutParams.MATCH_PARENT,
1222                             FrameLayout.LayoutParams.WRAP_CONTENT,
1223                         )
1224                 }
1225             },
1226             update = { view -> view.update() },
1227             onReset = {},
1228         )
1229     }
1230 }
1231 
1232 @Composable
1233 @VisibleForTesting
QuickQuickSettingsLayoutnull1234 fun QuickQuickSettingsLayout(
1235     tiles: @Composable () -> Unit,
1236     media: @Composable () -> Unit,
1237     mediaInRow: Boolean,
1238 ) {
1239     if (mediaInRow) {
1240         Row(
1241             horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
1242             verticalAlignment = Alignment.CenterVertically,
1243         ) {
1244             Box(modifier = Modifier.weight(1f)) { tiles() }
1245             Box(modifier = Modifier.weight(1f)) { media() }
1246         }
1247     } else {
1248         Column(verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical))) {
1249             tiles()
1250             media()
1251         }
1252     }
1253 }
1254 
1255 @Composable
1256 @VisibleForTesting
QuickSettingsLayoutnull1257 fun QuickSettingsLayout(
1258     brightness: @Composable () -> Unit,
1259     tiles: @Composable () -> Unit,
1260     media: @Composable () -> Unit,
1261     mediaInRow: Boolean,
1262 ) {
1263     if (mediaInRow) {
1264         Column(
1265             verticalArrangement = spacedBy(QuickSettingsShade.Dimensions.Padding),
1266             horizontalAlignment = Alignment.CenterHorizontally,
1267         ) {
1268             brightness()
1269             Row(
1270                 horizontalArrangement = spacedBy(QuickSettingsShade.Dimensions.Padding),
1271                 verticalAlignment = Alignment.CenterVertically,
1272             ) {
1273                 Box(modifier = Modifier.weight(1f)) { tiles() }
1274                 Box(modifier = Modifier.weight(1f)) { media() }
1275             }
1276         }
1277     } else {
1278         Column(
1279             verticalArrangement = spacedBy(QuickSettingsShade.Dimensions.Padding),
1280             horizontalAlignment = Alignment.CenterHorizontally,
1281         ) {
1282             brightness()
1283             tiles()
1284             media()
1285         }
1286     }
1287 }
1288 
1289 private object ResIdTags {
1290     const val quickSettingsPanel = "quick_settings_panel"
1291     const val quickQsPanel = "quick_qs_panel"
1292     const val qsScroll = "expanded_qs_scroll_view"
1293     const val qsFooterActions = "qs_footer_actions"
1294 }
1295 
qsHorizontalMarginnull1296 @Composable private fun qsHorizontalMargin() = dimensionResource(id = R.dimen.qs_horizontal_margin)
1297 
1298 @Composable
1299 private fun interactionsConfig() =
1300     InteractionsConfig(
1301         hoverOverlayColor = MaterialTheme.colorScheme.onSurface,
1302         hoverOverlayAlpha = 0.11f,
1303         pressedOverlayColor = MaterialTheme.colorScheme.onSurface,
1304         pressedOverlayAlpha = 0.15f,
1305         // we are OK using this as our content is clipped and all corner radius are larger than this
1306         surfaceCornerRadius = 28.dp,
1307     )
1308 
1309 private inline val alwaysCompose
1310     get() = Flags.alwaysComposeQsUiFragment()
1311 
1312 /**
1313  * Forces the configuration and themes to be dark theme. This is needed in order to have
1314  * [colorResource] retrieve the dark mode colors.
1315  *
1316  * This should be removed when [notificationShadeBlur] is removed
1317  */
1318 @Composable
1319 private fun AlwaysDarkMode(content: @Composable () -> Unit) {
1320     if (notificationShadeBlur()) {
1321         content()
1322     } else {
1323         val currentConfig = LocalConfiguration.current
1324         val darkConfig =
1325             Configuration(currentConfig).apply {
1326                 uiMode =
1327                     (uiMode and (Configuration.UI_MODE_NIGHT_MASK.inv())) or
1328                         Configuration.UI_MODE_NIGHT_YES
1329             }
1330         val newContext = LocalContext.current.createConfigurationContext(darkConfig)
1331         CompositionLocalProvider(
1332             LocalConfiguration provides darkConfig,
1333             LocalContext provides newContext,
1334         ) {
1335             content()
1336         }
1337     }
1338 }
1339