xref: /aosp_15_r20/frameworks/base/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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.adapter
18 
19 import android.content.Context
20 import android.content.pm.ActivityInfo
21 import android.os.Bundle
22 import android.view.View
23 import androidx.annotation.VisibleForTesting
24 import androidx.asynclayoutinflater.view.AsyncLayoutInflater
25 import com.android.app.tracing.coroutines.launchTraced as launch
26 import com.android.settingslib.applications.InterestingConfigChanges
27 import com.android.systemui.Dumpable
28 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
29 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.dagger.qualifiers.Main
33 import com.android.systemui.dump.DumpManager
34 import com.android.systemui.plugins.qs.QSContainerController
35 import com.android.systemui.qs.QSContainerImpl
36 import com.android.systemui.qs.QSImpl
37 import com.android.systemui.qs.dagger.QSSceneComponent
38 import com.android.systemui.res.R
39 import com.android.systemui.settings.brightness.MirrorController
40 import com.android.systemui.shade.ShadeDisplayAware
41 import com.android.systemui.shade.domain.interactor.ShadeInteractor
42 import com.android.systemui.shade.shared.model.ShadeMode
43 import com.android.systemui.util.kotlin.sample
44 import java.io.PrintWriter
45 import javax.inject.Inject
46 import javax.inject.Provider
47 import kotlin.coroutines.resume
48 import kotlin.coroutines.suspendCoroutine
49 import kotlinx.coroutines.CoroutineDispatcher
50 import kotlinx.coroutines.CoroutineScope
51 import kotlinx.coroutines.channels.BufferOverflow
52 import kotlinx.coroutines.flow.MutableSharedFlow
53 import kotlinx.coroutines.flow.MutableStateFlow
54 import kotlinx.coroutines.flow.SharingStarted
55 import kotlinx.coroutines.flow.StateFlow
56 import kotlinx.coroutines.flow.asStateFlow
57 import kotlinx.coroutines.flow.combine
58 import kotlinx.coroutines.flow.filterNotNull
59 import kotlinx.coroutines.flow.map
60 import kotlinx.coroutines.flow.stateIn
61 import kotlinx.coroutines.flow.update
62 import kotlinx.coroutines.withContext
63 
64 // TODO(307945185) Split View concerns into a ViewBinder
65 /** Adapter to use between Scene system and [QSImpl] */
66 interface QSSceneAdapter {
67 
68     /**
69      * Whether we are currently customizing or entering the customizer.
70      *
71      * @see CustomizerState.isCustomizing
72      */
73     val isCustomizing: StateFlow<Boolean>
74 
75     /**
76      * Whether the customizer is showing. This includes animating into and out of it.
77      *
78      * @see CustomizerState.isShowing
79      */
80     val isCustomizerShowing: StateFlow<Boolean>
81 
82     /**
83      * The duration of the current animation in/out of customizer. If not in an animating state,
84      * this duration is 0 (to match show/hide immediately).
85      *
86      * @see CustomizerState.Animating.animationDuration
87      */
88     val customizerAnimationDuration: StateFlow<Int>
89 
90     /**
91      * A view with the QS content ([QSContainerImpl]), managed by an instance of [QSImpl] tracked by
92      * the interactor.
93      *
94      * A null value means that there is no inflated view yet. See [inflate].
95      */
96     val qsView: StateFlow<View?>
97 
98     /** Sets the [MirrorController] in [QSImpl]. Set to `null` to remove. */
99     fun setBrightnessMirrorController(mirrorController: MirrorController?)
100 
101     /**
102      * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in
103      * [qsView]. Re-inflations due to configuration changes will use the last used [context].
104      */
105     suspend fun inflate(context: Context)
106 
107     /**
108      * Set the current state for QS. [state].
109      *
110      * This will not trigger expansion (animation between QQS or QS) or squishiness to be applied.
111      * For that, use [applyLatestExpansionAndSquishiness] outside of the composition phase.
112      */
113     fun setState(state: State)
114 
115     /**
116      * Explicitly applies the expansion and squishiness value from the latest state set. Call this
117      * only outside of the composition phase as this will call [QSImpl.setQsExpansion] that is
118      * normally called during animations. In particular, this will read the value of
119      * [State.squishiness], that is not safe to read in the composition phase.
120      */
121     fun applyLatestExpansionAndSquishiness()
122 
123     /** Propagates the bottom nav bar size to [QSImpl] to be used as necessary. */
124     suspend fun applyBottomNavBarPadding(padding: Int)
125 
126     /** The current height of QQS in the current [qsView], or 0 if there's no view. */
127     val qqsHeight: Int
128 
129     /** @return height with the squishiness fraction applied. */
130     val squishedQqsHeight: Int
131 
132     /**
133      * The current height of QS in the current [qsView], or 0 if there's no view. If customizing, it
134      * will return the height allocated to the customizer.
135      */
136     val qsHeight: Int
137 
138     /** @return height with the squishiness fraction applied. */
139     val squishedQsHeight: Int
140 
141     /** Compatibility for use by LockscreenShadeTransitionController. Matches default from [QS] */
142     val isQsFullyCollapsed: Boolean
143         get() = true
144 
145     /** Request that the customizer be closed. Possibly animating it. */
146     fun requestCloseCustomizer()
147 
148     sealed interface State {
149 
150         val isVisible: Boolean
151         val expansion: () -> Float
152         val squishiness: () -> Float
153 
154         data object CLOSED : State {
155             override val isVisible = false
156             override val expansion = { 0f }
157             override val squishiness = { 1f }
158         }
159 
160         /** State for expanding between QQS and QS */
161         class Expanding(override val expansion: () -> Float) : State {
162             override val isVisible = true
163             override val squishiness = { 1f }
164         }
165 
166         /**
167          * State for appearing QQS from Lockscreen or Gone.
168          *
169          * This should not be a data class, as it has a method parameter and even if it's the same
170          * lambda the output value may have changed.
171          */
172         class UnsquishingQQS(override val squishiness: () -> Float) : State {
173             override val isVisible = true
174             override val expansion = { 0f }
175         }
176 
177         /**
178          * State for appearing QS from Lockscreen or Gone, used in Split shade.
179          *
180          * This should not be a data class, as it has a method parameter and even if it's the same
181          * lambda the output value may have changed.
182          */
183         class UnsquishingQS(override val squishiness: () -> Float) : State {
184             override val isVisible = true
185             override val expansion = { 1f }
186         }
187 
188         companion object {
189             // These are special cases of the expansion.
190             val QQS = Expanding { 0f }
191             val QS = Expanding { 1f }
192 
193             /** Collapsing from QS to QQS. [progress] is 0f in QS and 1f in QQS. */
194             fun Collapsing(progress: () -> Float) = Expanding { 1f - progress() }
195         }
196     }
197 }
198 
199 @SysUISingleton
200 class QSSceneAdapterImpl
201 @VisibleForTesting
202 constructor(
203     private val qsSceneComponentFactory: QSSceneComponent.Factory,
204     private val qsImplProvider: Provider<QSImpl>,
205     shadeInteractor: ShadeInteractor,
206     displayStateInteractor: DisplayStateInteractor,
207     dumpManager: DumpManager,
208     @Main private val mainDispatcher: CoroutineDispatcher,
209     @Application applicationScope: CoroutineScope,
210     @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor,
211     private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
212 ) : QSContainerController, QSSceneAdapter, Dumpable {
213 
214     @Inject
215     constructor(
216         qsSceneComponentFactory: QSSceneComponent.Factory,
217         qsImplProvider: Provider<QSImpl>,
218         shadeInteractor: ShadeInteractor,
219         displayStateInteractor: DisplayStateInteractor,
220         dumpManager: DumpManager,
221         @Main dispatcher: CoroutineDispatcher,
222         @Application scope: CoroutineScope,
223         @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
224     ) : this(
225         qsSceneComponentFactory,
226         qsImplProvider,
227         shadeInteractor,
228         displayStateInteractor,
229         dumpManager,
230         dispatcher,
231         scope,
232         configurationInteractor,
233         ::AsyncLayoutInflater,
234     )
235 
236     private val bottomNavBarSize =
237         MutableSharedFlow<Int>(
238             extraBufferCapacity = 1,
239             onBufferOverflow = BufferOverflow.DROP_OLDEST,
240         )
241     private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
242     private val _customizingState: MutableStateFlow<CustomizerState> =
243         MutableStateFlow(CustomizerState.Hidden)
244     val customizerState = _customizingState.asStateFlow()
245 
246     override val isCustomizing: StateFlow<Boolean> =
247         customizerState
<lambda>null248             .map { it.isCustomizing }
249             .stateIn(
250                 applicationScope,
251                 SharingStarted.WhileSubscribed(),
252                 customizerState.value.isCustomizing,
253             )
254     override val isCustomizerShowing: StateFlow<Boolean> =
255         customizerState
<lambda>null256             .map { it.isShowing }
257             .stateIn(
258                 applicationScope,
259                 SharingStarted.WhileSubscribed(),
260                 customizerState.value.isShowing,
261             )
262     override val customizerAnimationDuration: StateFlow<Int> =
263         customizerState
<lambda>null264             .map { (it as? CustomizerState.Animating)?.animationDuration?.toInt() ?: 0 }
265             .stateIn(
266                 applicationScope,
267                 SharingStarted.WhileSubscribed(),
268                 (customizerState.value as? CustomizerState.Animating)?.animationDuration?.toInt()
269                     ?: 0,
270             )
271 
272     private val _qsImpl: MutableStateFlow<QSImpl?> = MutableStateFlow(null)
273     val qsImpl = _qsImpl.asStateFlow()
274     override val qsView: StateFlow<View?> =
275         _qsImpl
<lambda>null276             .map { it?.view }
277             .stateIn(applicationScope, SharingStarted.WhileSubscribed(), _qsImpl.value?.view)
278 
279     override val qqsHeight: Int
280         get() = qsImpl.value?.qqsHeight ?: 0
281 
282     override val squishedQqsHeight: Int
283         get() = qsImpl.value?.squishedQqsHeight ?: 0
284 
285     override val qsHeight: Int
286         get() = qsImpl.value?.qsHeight ?: 0
287 
288     override val squishedQsHeight: Int
289         get() = qsImpl.value?.squishedQsHeight ?: 0
290 
291     // If value is null, there's no QS and therefore it's fully collapsed.
292     override val isQsFullyCollapsed: Boolean
293         get() = qsImpl.value?.isFullyCollapsed ?: true
294 
295     // Same config changes as in FragmentHostManager
296     private val interestingChanges =
297         InterestingConfigChanges(
298             ActivityInfo.CONFIG_FONT_SCALE or
299                 ActivityInfo.CONFIG_LOCALE or
300                 ActivityInfo.CONFIG_ASSETS_PATHS
301         )
302 
303     init {
304         dumpManager.registerDumpable(this)
<lambda>null305         applicationScope.launch {
306             launch {
307                 state.sample(_customizingState, ::Pair).collect { (state, customizing) ->
308                     qsImpl.value?.apply {
309                         if (state != QSSceneAdapter.State.QS && customizing.isShowing) {
310                             this@apply.closeCustomizerImmediately()
311                         }
312                         applyState(state)
313                     }
314                 }
315             }
316             launch {
317                 configurationInteractor.configurationValues.collect { config ->
318                     if (interestingChanges.applyNewConfig(config)) {
319                         // Assumption: The context is always the same and with the same theme.
320                         // If colors change they will be reflected as attributes in the theme.
321                         qsImpl.value?.view?.let { inflate(it.context) }
322                     } else {
323                         qsImpl.value?.onConfigurationChanged(config)
324                         qsImpl.value?.view?.dispatchConfigurationChanged(config)
325                     }
326                 }
327             }
328             launch {
329                 combine(bottomNavBarSize, qsImpl.filterNotNull(), ::Pair).collect {
330                     it.second.applyBottomNavBarToCustomizerPadding(it.first)
331                 }
332             }
333             launch {
334                 shadeInteractor.shadeMode.collect {
335                     qsImpl.value?.setInSplitShade(it == ShadeMode.Split)
336                 }
337             }
338             launch {
339                 combine(displayStateInteractor.isLargeScreen, qsImpl.filterNotNull(), ::Pair)
340                     .collect { it.second.setIsNotificationPanelFullWidth(!it.first) }
341             }
342         }
343     }
344 
setCustomizerAnimatingnull345     override fun setCustomizerAnimating(animating: Boolean) {
346         if (_customizingState.value is CustomizerState.Animating && !animating) {
347             _customizingState.update {
348                 if (it is CustomizerState.AnimatingIntoCustomizer) {
349                     CustomizerState.Showing
350                 } else {
351                     CustomizerState.Hidden
352                 }
353             }
354         }
355     }
356 
setCustomizerShowingnull357     override fun setCustomizerShowing(showing: Boolean) {
358         setCustomizerShowing(showing, 0L)
359     }
360 
setCustomizerShowingnull361     override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
362         _customizingState.update { _ ->
363             if (showing) {
364                 if (animationDuration > 0) {
365                     CustomizerState.AnimatingIntoCustomizer(animationDuration)
366                 } else {
367                     CustomizerState.Showing
368                 }
369             } else {
370                 if (animationDuration > 0) {
371                     CustomizerState.AnimatingOutOfCustomizer(animationDuration)
372                 } else {
373                     CustomizerState.Hidden
374                 }
375             }
376         }
377     }
378 
setDetailShowingnull379     override fun setDetailShowing(showing: Boolean) {}
380 
inflatenull381     override suspend fun inflate(context: Context) {
382         withContext(mainDispatcher) {
383             val inflater = asyncLayoutInflaterFactory(context)
384             val view = suspendCoroutine { continuation ->
385                 inflater.inflate(R.layout.qs_panel, null) { view, _, _ ->
386                     continuation.resume(view)
387                 }
388             }
389             val bundle = Bundle()
390             _qsImpl.value?.onSaveInstanceState(bundle)
391             _qsImpl.value?.onDestroy()
392             val component = qsSceneComponentFactory.create(view)
393             val qs = qsImplProvider.get()
394             qs.onCreate(null)
395             qs.onComponentCreated(component, bundle)
396             _qsImpl.value = qs
397             qs.view.setPadding(0, 0, 0, 0)
398             qs.setContainerController(this@QSSceneAdapterImpl)
399             qs.applyState(state.value)
400             applyLatestExpansionAndSquishiness()
401         }
402     }
403 
setStatenull404     override fun setState(state: QSSceneAdapter.State) {
405         this.state.value = state
406     }
407 
applyBottomNavBarPaddingnull408     override suspend fun applyBottomNavBarPadding(padding: Int) {
409         bottomNavBarSize.emit(padding)
410     }
411 
requestCloseCustomizernull412     override fun requestCloseCustomizer() {
413         qsImpl.value?.closeCustomizer()
414     }
415 
setBrightnessMirrorControllernull416     override fun setBrightnessMirrorController(mirrorController: MirrorController?) {
417         qsImpl.value?.setBrightnessMirrorController(mirrorController)
418     }
419 
applyStatenull420     private fun QSImpl.applyState(state: QSSceneAdapter.State) {
421         setQsVisible(state.isVisible)
422         setExpanded(state.isVisible && state.expansion() > 0f)
423         setListening(state.isVisible)
424     }
425 
applyLatestExpansionAndSquishinessnull426     override fun applyLatestExpansionAndSquishiness() {
427         val qsImpl = _qsImpl.value
428         val state = state.value
429         qsImpl?.setQsExpansion(state.expansion(), 1f, 0f, state.squishiness())
430     }
431 
dumpnull432     override fun dump(pw: PrintWriter, args: Array<out String>) {
433         pw.apply {
434             println("Last state: ${state.value}")
435             println("CustomizerState: ${_customizingState.value}")
436             println("QQS height: $qqsHeight")
437             println("QS height: $qsHeight")
438         }
439     }
440 }
441 
442 /** Current state of the customizer */
443 sealed interface CustomizerState {
444 
445     /**
446      * This indicates that some part of the customizer is showing. It could be animating in or out.
447      */
448     val isShowing: Boolean
449         get() = true
450 
451     /**
452      * This indicates that we are currently customizing or animating into it. In particular, when
453      * animating out, this is false.
454      *
455      * @see QSCustomizer.isCustomizing
456      */
457     val isCustomizing: Boolean
458         get() = false
459 
460     sealed interface Animating : CustomizerState {
461         val animationDuration: Long
462     }
463 
464     /** Customizer is completely hidden, and not animating */
465     data object Hidden : CustomizerState {
466         override val isShowing = false
467     }
468 
469     /** Customizer is completely showing, and not animating */
470     data object Showing : CustomizerState {
471         override val isCustomizing = true
472     }
473 
474     /** Animating from [Hidden] into [Showing]. */
475     data class AnimatingIntoCustomizer(override val animationDuration: Long) : Animating {
476         override val isCustomizing = true
477     }
478 
479     /** Animating from [Showing] into [Hidden]. */
480     data class AnimatingOutOfCustomizer(override val animationDuration: Long) : Animating
481 }
482