• 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.viewmodel
18 
19 import android.content.res.Resources
20 import android.graphics.Rect
21 import androidx.annotation.FloatRange
22 import androidx.annotation.VisibleForTesting
23 import androidx.compose.runtime.derivedStateOf
24 import androidx.compose.runtime.getValue
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.setValue
27 import androidx.compose.runtime.snapshotFlow
28 import androidx.lifecycle.LifecycleCoroutineScope
29 import com.android.app.animation.Interpolators
30 import com.android.app.tracing.coroutines.launchTraced as launch
31 import com.android.internal.logging.UiEventLogger
32 import com.android.keyguard.BouncerPanelExpansionCalculator
33 import com.android.systemui.Dumpable
34 import com.android.systemui.animation.ShadeInterpolation
35 import com.android.systemui.classifier.Classifier
36 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
37 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
38 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
39 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
40 import com.android.systemui.keyguard.shared.model.Edge
41 import com.android.systemui.keyguard.shared.model.KeyguardState
42 import com.android.systemui.lifecycle.ExclusiveActivatable
43 import com.android.systemui.lifecycle.Hydrator
44 import com.android.systemui.log.table.TableLogBuffer
45 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS
46 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
47 import com.android.systemui.media.controls.ui.view.MediaHost
48 import com.android.systemui.media.controls.ui.view.MediaHostState
49 import com.android.systemui.media.dagger.MediaModule.QS_PANEL
50 import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
51 import com.android.systemui.plugins.statusbar.StatusBarStateController
52 import com.android.systemui.qs.FooterActionsController
53 import com.android.systemui.qs.QSEvent
54 import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeLog
55 import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule
56 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
57 import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
58 import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel
59 import com.android.systemui.qs.panels.ui.viewmodel.MediaInRowInLandscapeViewModel
60 import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
61 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
62 import com.android.systemui.res.R
63 import com.android.systemui.scene.shared.model.Overlays
64 import com.android.systemui.shade.LargeScreenHeaderHelper
65 import com.android.systemui.shade.ShadeDisplayAware
66 import com.android.systemui.shade.domain.interactor.ShadeInteractor
67 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
68 import com.android.systemui.statusbar.StatusBarState
69 import com.android.systemui.statusbar.SysuiStatusBarStateController
70 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
71 import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
72 import com.android.systemui.util.LargeScreenUtils
73 import com.android.systemui.util.asIndenting
74 import com.android.systemui.util.kotlin.emitOnStart
75 import com.android.systemui.util.printSection
76 import com.android.systemui.util.println
77 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
78 import dagger.assisted.Assisted
79 import dagger.assisted.AssistedFactory
80 import dagger.assisted.AssistedInject
81 import java.io.PrintWriter
82 import javax.inject.Named
83 import kotlinx.coroutines.awaitCancellation
84 import kotlinx.coroutines.channels.awaitClose
85 import kotlinx.coroutines.coroutineScope
86 import kotlinx.coroutines.flow.Flow
87 import kotlinx.coroutines.flow.callbackFlow
88 import kotlinx.coroutines.flow.flowOf
89 import kotlinx.coroutines.flow.map
90 import kotlinx.coroutines.flow.onStart
91 
92 class QSFragmentComposeViewModel
93 @AssistedInject
94 constructor(
95     containerViewModelFactory: QuickSettingsContainerViewModel.Factory,
96     @ShadeDisplayAware private val resources: Resources,
97     quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory,
98     footerActionsViewModelFactory: FooterActionsViewModel.Factory,
99     private val footerActionsController: FooterActionsController,
100     private val sysuiStatusBarStateController: SysuiStatusBarStateController,
101     deviceEntryInteractor: DeviceEntryInteractor,
102     disableFlagsInteractor: DisableFlagsInteractor,
103     keyguardTransitionInteractor: KeyguardTransitionInteractor,
104     private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
105     shadeInteractor: ShadeInteractor,
106     @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
107     private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
108     private val squishinessInteractor: TileSquishinessInteractor,
109     private val falsingInteractor: FalsingInteractor,
110     private val inFirstPageViewModel: InFirstPageViewModel,
111     @QSFragmentComposeLog private val tableLogBuffer: TableLogBuffer,
112     mediaInRowInLandscapeViewModelFactory: MediaInRowInLandscapeViewModel.Factory,
113     @Named(QUICK_QS_PANEL) val qqsMediaHost: MediaHost,
114     @Named(QS_PANEL) val qsMediaHost: MediaHost,
115     @Named(QSFragmentComposeModule.QS_USING_MEDIA_PLAYER) private val usingMedia: Boolean,
116     private val uiEventLogger: UiEventLogger,
117     @Assisted private val lifecycleScope: LifecycleCoroutineScope,
118 ) : Dumpable, ExclusiveActivatable() {
119 
120     val containerViewModel = containerViewModelFactory.create(supportsBrightnessMirroring = true)
121     val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create()
122 
123     private val qqsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QQS)
124     private val qsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QS)
125 
126     private val hydrator = Hydrator("QSFragmentComposeViewModel.hydrator", tableLogBuffer)
127 
128     val footerActionsViewModel =
129         footerActionsViewModelFactory.create(lifecycleScope).also {
130             lifecycleScope.launch { footerActionsController.init() }
131         }
132 
133     var isQsExpanded by mutableStateOf(false)
134 
135     var isQsVisible by mutableStateOf(false)
136 
137     val isQsVisibleAndAnyShadeExpanded: Boolean
138         get() = anyShadeExpanded && isQsVisible
139 
140     // This can only be negative if undefined (in which case it will be -1f), else it will be
141     // in [0, 1]. In some cases, it could be set back to -1f internally to indicate that it's
142     // different to every value in [0, 1].
143     private var qsExpansion by mutableStateOf(-1f)
144 
145     fun setQsExpansionValue(value: Float) {
146         if (value < 0f) {
147             qsExpansion = -1f
148         } else {
149             qsExpansion = value.coerceIn(0f, 1f)
150         }
151     }
152 
153     val isQsFullyCollapsed by derivedStateOf { qsExpansion <= 0f }
154 
155     var panelExpansionFraction by mutableStateOf(0f)
156 
157     var squishinessFraction by mutableStateOf(1f)
158 
159     val qqsHeaderHeight by
160         hydrator.hydratedStateOf(
161             traceName = "qqsHeaderHeight",
162             initialValue = 0,
163             source =
164                 configurationInteractor.onAnyConfigurationChange.map {
165                     if (LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)) {
166                         0
167                     } else {
168                         largeScreenHeaderHelper.getLargeScreenHeaderHeight()
169                     }
170                 },
171         )
172 
173     val qqsBottomPadding by
174         hydrator.hydratedStateOf(
175             traceName = "qqsBottomPadding",
176             initialValue = resources.getDimensionPixelSize(R.dimen.qqs_layout_padding_bottom),
177             source = configurationInteractor.dimensionPixelSize(R.dimen.qqs_layout_padding_bottom),
178         )
179 
180     // Starting with a non-zero value makes it so that it has a non-zero height on first expansion
181     // This is important for `QuickSettingsControllerImpl.mMinExpansionHeight` to detect a "change".
182     var qqsHeight by mutableStateOf(1)
183 
184     var qsScrollHeight by mutableStateOf(0)
185 
186     val heightDiff: Int
187         get() = qsScrollHeight - qqsHeight + qqsBottomPadding
188 
189     var isStackScrollerOverscrolling by mutableStateOf(false)
190 
191     var proposedTranslation by mutableStateOf(0f)
192 
193     /**
194      * Whether QS is enabled by policy. This is normally true, except when it's disabled by some
195      * policy. See [DisableFlagsRepository].
196      */
197     val isQsEnabled by
198         hydrator.hydratedStateOf(
199             traceName = "isQsEnabled",
200             initialValue = disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(),
201             source = disableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() },
202         )
203 
204     var isInSplitShade by mutableStateOf(false)
205 
206     var isTransitioningToFullShade by mutableStateOf(false)
207 
208     var lockscreenToShadeProgress by mutableStateOf(0f)
209 
210     var isSmallScreen by mutableStateOf(false)
211 
212     var heightOverride by mutableStateOf(-1)
213 
214     val expansionState by derivedStateOf {
215         if (forceQs) {
216             QSExpansionState(1f)
217         } else {
218             QSExpansionState(qsExpansion.coerceIn(0f, 1f))
219         }
220     }
221 
222     val isQsFullyExpanded by derivedStateOf { expansionState.progress >= 1f && isQsExpanded }
223 
224     /**
225      * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for
226      * determining the correct action based on the expansion state.
227      */
228     var collapseExpandAccessibilityAction: Runnable? = null
229 
230     var overScrollAmount by mutableStateOf(0)
231 
232     val viewTranslationY by derivedStateOf {
233         if (isOverscrolling) {
234             overScrollAmount.toFloat()
235         } else {
236             if (onKeyguardAndExpanded) {
237                 translationScaleY * qqsHeight
238             } else {
239                 headerTranslation
240             }
241         }
242     }
243 
244     val qsScrollTranslationY by derivedStateOf {
245         val panelTranslationY = translationScaleY * heightDiff
246         if (onKeyguardAndExpanded) panelTranslationY else 0f
247     }
248 
249     val viewAlpha by derivedStateOf {
250         when {
251             isInBouncerTransit ->
252                 BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(alphaProgress)
253             isKeyguardState -> alphaProgress
254             isSmallScreen -> ShadeInterpolation.getContentAlpha(alphaProgress)
255             else -> largeScreenShadeInterpolator.getQsAlpha(alphaProgress)
256         }
257     }
258 
259     val showingMirror: Boolean
260         get() = containerViewModel.brightnessSliderViewModel.showMirror
261 
262     // The initial values in these two are not meaningful. The flow will emit on start the correct
263     // values. This is because we need to lazily fetch them after initMediaHosts.
264     val qqsMediaVisible by
265         hydrator.hydratedStateOf(
266             traceName = "qqsMediaVisible",
267             initialValue = usingMedia,
268             source =
269                 if (usingMedia) {
270                     mediaHostVisible(qqsMediaHost)
271                 } else {
272                     flowOf(false)
273                 },
274         )
275 
276     val qqsMediaInRow: Boolean
277         get() = qqsMediaInRowViewModel.shouldMediaShowInRow
278 
279     val qsMediaVisible by
280         hydrator.hydratedStateOf(
281             traceName = "qsMediaVisible",
282             initialValue = usingMedia,
283             source = if (usingMedia) mediaHostVisible(qsMediaHost) else flowOf(false),
284         )
285 
286     val qsMediaInRow: Boolean
287         get() = qsMediaInRowViewModel.shouldMediaShowInRow
288 
289     var shouldUpdateSquishinessOnMedia by mutableStateOf(false)
290 
291     val qsMediaTranslationY by derivedStateOf {
292         if (
293             qsExpansion > 0f &&
294                 !isKeyguardState &&
295                 !qqsMediaVisible &&
296                 !qsMediaInRow &&
297                 !isInSplitShade
298         ) {
299             val interpolation = Interpolators.ACCELERATE.getInterpolation(1f - qsExpansion)
300             -qsMediaHost.hostView.height * 1.3f * interpolation
301         } else {
302             0f
303         }
304     }
305 
306     val animateTilesExpansion: Boolean
307         get() = inFirstPage && !mediaSuddenlyAppearingInLandscape
308 
309     val isEditing by
310         hydrator.hydratedStateOf(
311             traceName = "isEditing",
312             source = containerViewModel.editModeViewModel.isEditing,
313         )
314 
315     /** True if we are not in an expansion (from Gone to QQS/QS) animation. */
316     val isNotTransitioning by derivedStateOf {
317         viewTranslationY == 0f && viewAlpha == 1f && constrainedSquishinessFraction == 1f
318     }
319 
320     private val inFirstPage: Boolean
321         get() = inFirstPageViewModel.inFirstPage
322 
323     private val mediaSuddenlyAppearingInLandscape: Boolean
324         get() = !qqsMediaInRow && qsMediaInRow
325 
326     private val collapsedLandscapeMedia by
327         hydrator.hydratedStateOf(
328             traceName = "collapsedLandscapeMedia",
329             initialValue = resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed),
330             source =
331                 configurationInteractor.onAnyConfigurationChange.emitOnStart().map {
332                     resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed)
333                 },
334         )
335 
336     private val qqsMediaExpansion: Float
337         get() =
338             if (qqsMediaInRow && collapsedLandscapeMedia) {
339                 MediaHostState.COLLAPSED
340             } else {
341                 MediaHostState.EXPANDED
342             }
343 
344     private val shouldApplySquishinessToMedia by derivedStateOf {
345         shouldUpdateSquishinessOnMedia || (isInSplitShade && statusBarState == StatusBarState.SHADE)
346     }
347 
348     private val mediaSquishiness by derivedStateOf {
349         if (shouldApplySquishinessToMedia) {
350             squishinessFraction
351         } else {
352             1f
353         }
354     }
355 
356     private var qsBounds by mutableStateOf(Rect())
357 
358     private val constrainedSquishinessFraction: Float
359         get() = squishinessFraction.constrainSquishiness()
360 
361     private var _headerAnimating by mutableStateOf(false)
362 
363     /**
364      * Tracks the current [StatusBarState]. It will switch early if the upcoming state is
365      * [StatusBarState.KEYGUARD]
366      */
367     @get:VisibleForTesting
368     val statusBarState by
369         hydrator.hydratedStateOf(
370             traceName = "statusBarState",
371             initialValue = sysuiStatusBarStateController.state,
372             source =
373                 conflatedCallbackFlow {
374                         val callback =
375                             object : StatusBarStateController.StateListener {
376                                 override fun onStateChanged(newState: Int) {
377                                     trySend(newState)
378                                 }
379 
380                                 override fun onUpcomingStateChanged(upcomingState: Int) {
381                                     if (upcomingState == StatusBarState.KEYGUARD) {
382                                         trySend(upcomingState)
383                                     }
384                                 }
385                             }
386                         sysuiStatusBarStateController.addCallback(callback)
387 
388                         awaitClose { sysuiStatusBarStateController.removeCallback(callback) }
389                     }
390                     .onStart { emit(sysuiStatusBarStateController.state) },
391         )
392 
393     private val isKeyguardState: Boolean
394         get() = statusBarState == StatusBarState.KEYGUARD
395 
396     private var viewHeight by mutableStateOf(0)
397 
398     private val isBypassEnabled by
399         hydrator.hydratedStateOf(
400             traceName = "isBypassEnabled",
401             source = deviceEntryInteractor.isBypassEnabled,
402         )
403 
404     private val showCollapsedOnKeyguard by derivedStateOf {
405         isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade)
406     }
407 
408     private val onKeyguardAndExpanded: Boolean
409         get() = isKeyguardState && !showCollapsedOnKeyguard
410 
411     private val isOverscrolling: Boolean
412         get() = overScrollAmount != 0
413 
414     private val forceQs by derivedStateOf {
415         (isQsExpanded || isStackScrollerOverscrolling) &&
416             (isKeyguardState && !showCollapsedOnKeyguard)
417     }
418 
419     private val translationScaleY: Float
420         get() = ((qsExpansion - 1) * (if (isInSplitShade) 1f else SHORT_PARALLAX_AMOUNT))
421 
422     private val headerTranslation by derivedStateOf {
423         if (isTransitioningToFullShade) 0f else proposedTranslation
424     }
425 
426     private val alphaProgress by derivedStateOf {
427         when {
428             isSmallScreen -> 1f
429             isInSplitShade ->
430                 if (isTransitioningToFullShade || isKeyguardState) {
431                     lockscreenToShadeProgress
432                 } else {
433                     panelExpansionFraction
434                 }
435             isTransitioningToFullShade -> lockscreenToShadeProgress
436             else -> panelExpansionFraction
437         }
438     }
439 
440     private val isInBouncerTransit by
441         hydrator.hydratedStateOf(
442             traceName = "isInBouncerTransit",
443             initialValue = false,
444             source =
445                 keyguardTransitionInteractor.isInTransition(
446                     Edge.create(to = Overlays.Bouncer),
447                     Edge.create(to = KeyguardState.PRIMARY_BOUNCER),
448                 ),
449         )
450 
451     private val anyShadeExpanded by
452         hydrator.hydratedStateOf(
453             traceName = "anyShadeExpanded",
454             source = shadeInteractor.isAnyExpanded,
455         )
456 
457     fun applyNewQsScrollerBounds(left: Float, top: Float, right: Float, bottom: Float) {
458         if (usingMedia) {
459             qsMediaHost.currentClipping.set(
460                 left.toInt(),
461                 top.toInt(),
462                 right.toInt(),
463                 bottom.toInt(),
464             )
465         }
466     }
467 
468     fun emitMotionEventForFalsingSwipeNested() {
469         falsingInteractor.isFalseTouch(Classifier.QS_SWIPE_NESTED)
470     }
471 
472     fun onQQSOpen() {
473         uiEventLogger.log(QSEvent.QQS_PANEL_EXPANDED)
474     }
475 
476     fun onQSOpen() {
477         uiEventLogger.log(QSEvent.QS_PANEL_EXPANDED)
478     }
479 
480     override suspend fun onActivated(): Nothing {
481         initMediaHosts() // init regardless of using media (same as current QS).
482         coroutineScope {
483             launch { hydrateSquishinessInteractor() }
484             if (usingMedia) {
485                 launch { hydrateQqsMediaExpansion() }
486                 launch { hydrateMediaSquishiness() }
487                 launch { hydrateMediaDisappearParameters() }
488             }
489             launch { hydrator.activate() }
490             launch { containerViewModel.activate() }
491             launch { quickQuickSettingsViewModel.activate() }
492             launch { qqsMediaInRowViewModel.activate() }
493             launch { qsMediaInRowViewModel.activate() }
494             awaitCancellation()
495         }
496     }
497 
498     private fun initMediaHosts() {
499         qqsMediaHost.apply {
500             expansion = qqsMediaExpansion
501             showsOnlyActiveMedia = true
502             init(LOCATION_QQS)
503         }
504         qsMediaHost.apply {
505             expansion = MediaHostState.EXPANDED
506             showsOnlyActiveMedia = false
507             init(LOCATION_QS)
508         }
509     }
510 
511     private suspend fun hydrateSquishinessInteractor() {
512         snapshotFlow { constrainedSquishinessFraction }
513             .collect { squishinessInteractor.setSquishinessValue(it) }
514     }
515 
516     private suspend fun hydrateQqsMediaExpansion() {
517         snapshotFlow { qqsMediaExpansion }.collect { qqsMediaHost.expansion = it }
518     }
519 
520     private suspend fun hydrateMediaSquishiness() {
521         snapshotFlow { mediaSquishiness }.collect { qsMediaHost.squishFraction = it }
522     }
523 
524     private suspend fun hydrateMediaDisappearParameters() {
525         coroutineScope {
526             launch {
527                 snapshotFlow { qqsMediaInRow }.collect { qqsMediaHost.applyDisappearParameters(it) }
528             }
529             launch {
530                 snapshotFlow { qsMediaInRow }.collect { qsMediaHost.applyDisappearParameters(it) }
531             }
532         }
533     }
534 
535     override fun dump(pw: PrintWriter, args: Array<out String>) {
536         pw.asIndenting().run {
537             printSection("Quick Settings state") {
538                 println("isQSExpanded", isQsExpanded)
539                 println("isQSVisible", isQsVisible)
540                 println("anyShadeExpanded", anyShadeExpanded)
541                 println("isQSVisibleAndAnyShadeExpanded", isQsVisibleAndAnyShadeExpanded)
542                 println("isQSEnabled", isQsEnabled)
543                 println("isCustomizing", containerViewModel.editModeViewModel.isEditing.value)
544                 println("inFirstPage", inFirstPage)
545             }
546             printSection("Expansion state") {
547                 println("qsExpansion", qsExpansion)
548                 println("panelExpansionFraction", panelExpansionFraction)
549                 println("squishinessFraction", squishinessFraction)
550                 println("proposedTranslation", proposedTranslation)
551                 println("expansionState", expansionState)
552                 println("forceQS", forceQs)
553                 printSection("Derived values") {
554                     println("headerTranslation", headerTranslation)
555                     println("translationScaleY", translationScaleY)
556                     println("viewTranslationY", viewTranslationY)
557                     println("qsScrollTranslationY", qsScrollTranslationY)
558                     println("viewAlpha", viewAlpha)
559                 }
560             }
561             printSection("Shade state") {
562                 println("stackOverscrolling", isStackScrollerOverscrolling)
563                 println("overscrollAmount", overScrollAmount)
564                 println("statusBarState", StatusBarState.toString(statusBarState))
565                 println("isKeyguardState", isKeyguardState)
566                 println("isSmallScreen", isSmallScreen)
567                 println("heightOverride", "${heightOverride}px")
568                 println("qqsHeaderHeight", "${qqsHeaderHeight}px")
569                 println("qqsBottomPadding", "${qqsBottomPadding}px")
570                 println("isSplitShade", isInSplitShade)
571                 println("showCollapsedOnKeyguard", showCollapsedOnKeyguard)
572                 println("qqsHeight", "${qqsHeight}px")
573                 println("qsScrollHeight", "${qsScrollHeight}px")
574             }
575             printSection("Media") {
576                 println("qqsMediaVisible", qqsMediaVisible)
577                 println("qqsMediaInRow", qqsMediaInRow)
578                 println("qsMediaVisible", qsMediaVisible)
579                 println("qsMediaInRow", qsMediaInRow)
580                 println("collapsedLandscapeMedia", collapsedLandscapeMedia)
581                 println("qqsMediaExpansion", qqsMediaExpansion)
582                 println("shouldUpdateSquishinessOnMedia", shouldUpdateSquishinessOnMedia)
583                 println("mediaSquishiness", mediaSquishiness)
584                 println("qsMediaTranslationY", qsMediaTranslationY)
585             }
586         }
587     }
588 
589     @AssistedFactory
590     interface Factory {
591         fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel
592     }
593 
594     // In the future, this may have other relevant elements.
595     data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float)
596 }
597 
Floatnull598 private fun Float.constrainSquishiness(): Float {
599     return (0.1f + this * 0.9f).coerceIn(0f, 1f)
600 }
601 
602 private val SHORT_PARALLAX_AMOUNT = 0.1f
603 
604 /**
605  * Returns a flow to track the visibility of a [MediaHost]. The flow will emit on start the visible
606  * state of the view.
607  */
mediaHostVisiblenull608 private fun mediaHostVisible(mediaHost: MediaHost): Flow<Boolean> {
609     return callbackFlow {
610             val listener: (Boolean) -> Unit = { visible: Boolean -> trySend(visible) }
611             mediaHost.addVisibilityChangeListener(listener)
612 
613             awaitClose { mediaHost.removeVisibilityChangeListener(listener) }
614         }
615         // Need to use this to set initial state because on creation of the media host, the
616         // view visibility is not in sync with [MediaHost.visible], which is what we track with
617         // the listener. The correct state is set as part of init, so we need to get the state
618         // lazily.
619         .onStart { emit(mediaHost.visible) }
620 }
621 
622 // Taken from QSPanelControllerBase
applyDisappearParametersnull623 private fun MediaHost.applyDisappearParameters(inRow: Boolean) {
624     disappearParameters.apply {
625         fadeStartPosition = 0.95f
626         disappearStart = 0f
627         if (inRow) {
628             disappearSize.set(0f, 0.4f)
629             gonePivot.set(1f, 0f)
630             contentTranslationFraction.set(0.25f, 1f)
631             disappearEnd = 0.6f
632         } else {
633             disappearSize.set(1f, 0f)
634             gonePivot.set(0f, 0f)
635             contentTranslationFraction.set(0f, 1f)
636             disappearEnd = 0.95f
637         }
638     }
639 }
640