1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.qs.ui.composable
18 
19 import android.view.ViewGroup
20 import android.widget.FrameLayout
21 import androidx.compose.foundation.layout.Box
22 import androidx.compose.foundation.layout.fillMaxHeight
23 import androidx.compose.foundation.layout.fillMaxWidth
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.DisposableEffect
26 import androidx.compose.runtime.LaunchedEffect
27 import androidx.compose.runtime.getValue
28 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.draw.drawWithContent
30 import androidx.compose.ui.layout.layout
31 import androidx.compose.ui.platform.LocalContext
32 import androidx.compose.ui.unit.dp
33 import androidx.compose.ui.viewinterop.AndroidView
34 import androidx.lifecycle.compose.collectAsStateWithLifecycle
35 import com.android.compose.animation.scene.ElementKey
36 import com.android.compose.animation.scene.MovableElementScenePicker
37 import com.android.compose.animation.scene.SceneScope
38 import com.android.compose.animation.scene.TransitionState
39 import com.android.compose.animation.scene.ValueKey
40 import com.android.compose.modifiers.thenIf
41 import com.android.systemui.compose.modifiers.sysuiResTag
42 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
43 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
44 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
45 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
46 import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
47 import com.android.systemui.scene.shared.model.Scenes
48 
49 object QuickSettings {
50     private val SCENES =
51         setOf(
52             Scenes.QuickSettings,
53             Scenes.Shade,
54         )
55 
56     object Elements {
57         val Content =
58             ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
59         val QuickQuickSettings = ElementKey("QuickQuickSettings")
60         val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
61         val FooterActions = ElementKey("QuickSettingsFooterActions")
62     }
63 
64     object SharedValues {
65         val TilesSquishiness = ValueKey("QuickSettingsTileSquishiness")
66 
67         object SquishinessValues {
68             val Default = 1f
69             val LockscreenSceneStarting = 0f
70             val GoneSceneStarting = 0.3f
71         }
72 
73         val MediaLandscapeTopOffset = ValueKey("MediaLandscapeTopOffset")
74 
75         object MediaOffset {
76             val InQQS = 0.dp
77             // Brightness + padding
78             val InQS = 92.dp
79             val Default = 0.dp
80         }
81     }
82 }
83 
stateForQuickSettingsContentnull84 private fun SceneScope.stateForQuickSettingsContent(
85     isSplitShade: Boolean,
86     squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default }
87 ): QSSceneAdapter.State {
transitionStatenull88     return when (val transitionState = layoutState.transitionState) {
89         is TransitionState.Idle -> {
90             when (transitionState.currentScene) {
91                 Scenes.Shade ->
92                     QSSceneAdapter.State.QQS.takeUnless { isSplitShade } ?: QSSceneAdapter.State.QS
93                 Scenes.QuickSettings -> QSSceneAdapter.State.QS
94                 else -> QSSceneAdapter.State.CLOSED
95             }
96         }
97         is TransitionState.Transition ->
98             with(transitionState) {
99                 when {
100                     isSplitShade -> UnsquishingQS(squishiness)
101                     fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
102                         Expanding(progress)
103                     }
104                     fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
105                         Collapsing(progress)
106                     }
107                     fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
108                         UnsquishingQQS(squishiness)
109                     }
110                     fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
111                         QSSceneAdapter.State.QS
112                     }
113                     else ->
114                         error(
115                             "Bad transition for QuickSettings: fromScene=$fromScene," +
116                                 " toScene=$toScene"
117                         )
118                 }
119             }
120     }
121 }
122 
123 /**
124  * This composable will show QuickSettingsContent in the correct state (as determined by its
125  * [SceneScope]).
126  *
127  * If adding to scenes not in:
128  * * QuickSettingsScene
129  * * ShadeScene
130  *
131  * amend:
132  * * [stateForQuickSettingsContent],
133  * * [QuickSettings.SCENES],
134  * * this doc.
135  */
136 @Composable
QuickSettingsnull137 fun SceneScope.QuickSettings(
138     qsSceneAdapter: QSSceneAdapter,
139     heightProvider: () -> Int,
140     isSplitShade: Boolean,
141     modifier: Modifier = Modifier,
142     squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
143 ) {
<lambda>null144     val contentState = { stateForQuickSettingsContent(isSplitShade, squishiness) }
145     val transitionState = layoutState.transitionState
146     val isClosing =
147         transitionState is TransitionState.Transition &&
148             transitionState.progress >= 0.9f && // almost done closing
149             !(layoutState.isTransitioning(to = Scenes.Shade) ||
150                 layoutState.isTransitioning(to = Scenes.QuickSettings))
151 
152     if (isClosing) {
<lambda>null153         DisposableEffect(Unit) {
154             onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
155         }
156     }
157 
158     MovableElement(
159         key = QuickSettings.Elements.Content,
160         modifier =
161             modifier.sysuiResTag("quick_settings_panel").fillMaxWidth().layout {
measurablenull162                 measurable,
163                 constraints ->
164                 val placeable = measurable.measure(constraints)
165                 // Use the height of the correct view based on the scene it is being composed in
166                 val height = heightProvider().coerceAtLeast(0)
167 
168                 layout(placeable.width, height) { placeable.placeRelative(0, 0) }
169             }
<lambda>null170     ) {
171         content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
172     }
173 }
174 
175 @Composable
QuickSettingsContentnull176 private fun QuickSettingsContent(
177     qsSceneAdapter: QSSceneAdapter,
178     state: () -> QSSceneAdapter.State,
179     modifier: Modifier = Modifier,
180 ) {
181     val qsView by qsSceneAdapter.qsView.collectAsStateWithLifecycle()
182     val isCustomizing by qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
183     QuickSettingsTheme {
184         val context = LocalContext.current
185 
186         LaunchedEffect(key1 = context) {
187             if (qsView == null) {
188                 qsSceneAdapter.inflate(context)
189             }
190         }
191         qsView?.let { view ->
192             Box(
193                 modifier =
194                     modifier
195                         .fillMaxWidth()
196                         .thenIf(isCustomizing) { Modifier.fillMaxHeight() }
197                         .drawWithContent {
198                             qsSceneAdapter.applyLatestExpansionAndSquishiness()
199                             drawContent()
200                         }
201             ) {
202                 AndroidView(
203                     modifier = Modifier.fillMaxWidth(),
204                     factory = { context ->
205                         qsSceneAdapter.setState(state())
206                         FrameLayout(context).apply {
207                             (view.parent as? ViewGroup)?.removeView(view)
208                             addView(view)
209                         }
210                     },
211                     // When the view changes (e.g. due to a theme change), this will be recomposed
212                     // if needed and the new view will be attached to the FrameLayout here.
213                     update = {
214                         qsSceneAdapter.setState(state())
215                         if (view.parent != it) {
216                             it.removeAllViews()
217                             (view.parent as? ViewGroup)?.removeView(view)
218                             it.addView(view)
219                         }
220                     },
221                     onRelease = { it.removeAllViews() }
222                 )
223             }
224         }
225     }
226 }
227