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