1 /*
<lambda>null2  * Copyright (C) 2020 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.media
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import androidx.constraintlayout.widget.ConstraintSet
22 import com.android.systemui.R
23 import com.android.systemui.statusbar.policy.ConfigurationController
24 import com.android.systemui.util.animation.MeasurementOutput
25 import com.android.systemui.util.animation.TransitionLayout
26 import com.android.systemui.util.animation.TransitionLayoutController
27 import com.android.systemui.util.animation.TransitionViewState
28 import javax.inject.Inject
29 
30 /**
31  * A class responsible for controlling a single instance of a media player handling interactions
32  * with the view instance and keeping the media view states up to date.
33  */
34 class MediaViewController @Inject constructor(
35     context: Context,
36     private val configurationController: ConfigurationController,
37     private val mediaHostStatesManager: MediaHostStatesManager
38 ) {
39 
40     /**
41      * A listener when the current dimensions of the player change
42      */
43     lateinit var sizeChangedListener: () -> Unit
44     private var firstRefresh: Boolean = true
45     private var transitionLayout: TransitionLayout? = null
46     private val layoutController = TransitionLayoutController()
47     private var animationDelay: Long = 0
48     private var animationDuration: Long = 0
49     private var animateNextStateChange: Boolean = false
50     private val measurement = MeasurementOutput(0, 0)
51 
52     /**
53      * A map containing all viewStates for all locations of this mediaState
54      */
55     private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
56 
57     /**
58      * The ending location of the view where it ends when all animations and transitions have
59      * finished
60      */
61     @MediaLocation
62     private var currentEndLocation: Int = -1
63 
64     /**
65      * The ending location of the view where it ends when all animations and transitions have
66      * finished
67      */
68     @MediaLocation
69     private var currentStartLocation: Int = -1
70 
71     /**
72      * The progress of the transition or 1.0 if there is no transition happening
73      */
74     private var currentTransitionProgress: Float = 1.0f
75 
76     /**
77      * A temporary state used to store intermediate measurements.
78      */
79     private val tmpState = TransitionViewState()
80 
81     /**
82      * A temporary state used to store intermediate measurements.
83      */
84     private val tmpState2 = TransitionViewState()
85 
86     /**
87      * A temporary state used to store intermediate measurements.
88      */
89     private val tmpState3 = TransitionViewState()
90 
91     /**
92      * A temporary cache key to be used to look up cache entries
93      */
94     private val tmpKey = CacheKey()
95 
96     /**
97      * The current width of the player. This might not factor in case the player is animating
98      * to the current state, but represents the end state
99      */
100     var currentWidth: Int = 0
101     /**
102      * The current height of the player. This might not factor in case the player is animating
103      * to the current state, but represents the end state
104      */
105     var currentHeight: Int = 0
106 
107     /**
108      * Get the translationX of the layout
109      */
110     var translationX: Float = 0.0f
111         private set
112         get() {
113             return transitionLayout?.translationX ?: 0.0f
114         }
115 
116     /**
117      * Get the translationY of the layout
118      */
119     var translationY: Float = 0.0f
120         private set
121         get() {
122             return transitionLayout?.translationY ?: 0.0f
123         }
124 
125     /**
126      * A callback for RTL config changes
127      */
128     private val configurationListener = object : ConfigurationController.ConfigurationListener {
129         override fun onConfigChanged(newConfig: Configuration?) {
130             // Because the TransitionLayout is not always attached (and calculates/caches layout
131             // results regardless of attach state), we have to force the layoutDirection of the view
132             // to the correct value for the user's current locale to ensure correct recalculation
133             // when/after calling refreshState()
134             newConfig?.apply {
135                 if (transitionLayout?.rawLayoutDirection != layoutDirection) {
136                     transitionLayout?.layoutDirection = layoutDirection
137                     refreshState()
138                 }
139             }
140         }
141     }
142 
143     /**
144      * A callback for media state changes
145      */
146     val stateCallback = object : MediaHostStatesManager.Callback {
147         override fun onHostStateChanged(
148             @MediaLocation location: Int,
149             mediaHostState: MediaHostState
150         ) {
151             if (location == currentEndLocation || location == currentStartLocation) {
152                 setCurrentState(currentStartLocation,
153                         currentEndLocation,
154                         currentTransitionProgress,
155                         applyImmediately = false)
156             }
157         }
158     }
159 
160     /**
161      * The expanded constraint set used to render a expanded player. If it is modified, make sure
162      * to call [refreshState]
163      */
164     val collapsedLayout = ConstraintSet()
165 
166     /**
167      * The expanded constraint set used to render a collapsed player. If it is modified, make sure
168      * to call [refreshState]
169      */
170     val expandedLayout = ConstraintSet()
171 
172     init {
173         collapsedLayout.load(context, R.xml.media_collapsed)
174         expandedLayout.load(context, R.xml.media_expanded)
175         mediaHostStatesManager.addController(this)
176         layoutController.sizeChangedListener = { width: Int, height: Int ->
177             currentWidth = width
178             currentHeight = height
179             sizeChangedListener.invoke()
180         }
181         configurationController.addCallback(configurationListener)
182     }
183 
184     /**
185      * Notify this controller that the view has been removed and all listeners should be destroyed
186      */
187     fun onDestroy() {
188         mediaHostStatesManager.removeController(this)
189         configurationController.removeCallback(configurationListener)
190     }
191 
192     private fun ensureAllMeasurements() {
193         val mediaStates = mediaHostStatesManager.mediaHostStates
194         for (entry in mediaStates) {
195             obtainViewState(entry.value)
196         }
197     }
198 
199     /**
200      * Get the constraintSet for a given expansion
201      */
202     private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
203             if (expansion > 0) expandedLayout else collapsedLayout
204 
205     /**
206      * Obtain a new viewState for a given media state. This usually returns a cached state, but if
207      * it's not available, it will recreate one by measuring, which may be expensive.
208      */
209     private fun obtainViewState(state: MediaHostState?): TransitionViewState? {
210         if (state == null || state.measurementInput == null) {
211             return null
212         }
213         // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
214         var cacheKey = getKey(state, tmpKey)
215         val viewState = viewStates[cacheKey]
216         if (viewState != null) {
217             // we already have cached this measurement, let's continue
218             return viewState
219         }
220         // Copy the key since this might call recursively into it and we're using tmpKey
221         cacheKey = cacheKey.copy()
222         val result: TransitionViewState?
223         if (transitionLayout != null) {
224             // Let's create a new measurement
225             if (state.expansion == 0.0f || state.expansion == 1.0f) {
226                 result = transitionLayout!!.calculateViewState(
227                         state.measurementInput!!,
228                         constraintSetForExpansion(state.expansion),
229                         TransitionViewState())
230 
231                 // We don't want to cache interpolated or null states as this could quickly fill up
232                 // our cache. We only cache the start and the end states since the interpolation
233                 // is cheap
234                 viewStates[cacheKey] = result
235             } else {
236                 // This is an interpolated state
237                 val startState = state.copy().also { it.expansion = 0.0f }
238 
239                 // Given that we have a measurement and a view, let's get (guaranteed) viewstates
240                 // from the start and end state and interpolate them
241                 val startViewState = obtainViewState(startState) as TransitionViewState
242                 val endState = state.copy().also { it.expansion = 1.0f }
243                 val endViewState = obtainViewState(endState) as TransitionViewState
244                 result = layoutController.getInterpolatedState(
245                         startViewState,
246                         endViewState,
247                         state.expansion)
248             }
249         } else {
250             result = null
251         }
252         return result
253     }
254 
255     private fun getKey(state: MediaHostState, result: CacheKey): CacheKey {
256         result.apply {
257             heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
258             widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
259             expansion = state.expansion
260         }
261         return result
262     }
263 
264     /**
265      * Attach a view to this controller. This may perform measurements if it's not available yet
266      * and should therefore be done carefully.
267      */
268     fun attach(transitionLayout: TransitionLayout) {
269         this.transitionLayout = transitionLayout
270         layoutController.attach(transitionLayout)
271         if (currentEndLocation == -1) {
272             return
273         }
274         // Set the previously set state immediately to the view, now that it's finally attached
275         setCurrentState(
276                 startLocation = currentStartLocation,
277                 endLocation = currentEndLocation,
278                 transitionProgress = currentTransitionProgress,
279                 applyImmediately = true)
280     }
281 
282     /**
283      * Obtain a measurement for a given location. This makes sure that the state is up to date
284      * and all widgets know their location. Calling this method may create a measurement if we
285      * don't have a cached value available already.
286      */
287     fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? {
288         val viewState = obtainViewState(hostState) ?: return null
289         measurement.measuredWidth = viewState.width
290         measurement.measuredHeight = viewState.height
291         return measurement
292     }
293 
294     /**
295      * Set a new state for the controlled view which can be an interpolation between multiple
296      * locations.
297      */
298     fun setCurrentState(
299         @MediaLocation startLocation: Int,
300         @MediaLocation endLocation: Int,
301         transitionProgress: Float,
302         applyImmediately: Boolean
303     ) {
304         currentEndLocation = endLocation
305         currentStartLocation = startLocation
306         currentTransitionProgress = transitionProgress
307 
308         val shouldAnimate = animateNextStateChange && !applyImmediately
309 
310         val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
311         val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
312 
313         // Obtain the view state that we'd want to be at the end
314         // The view might not be bound yet or has never been measured and in that case will be
315         // reset once the state is fully available
316         var endViewState = obtainViewState(endHostState) ?: return
317         endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
318         layoutController.setMeasureState(endViewState)
319 
320         // If the view isn't bound, we can drop the animation, otherwise we'll execute it
321         animateNextStateChange = false
322         if (transitionLayout == null) {
323             return
324         }
325 
326         val result: TransitionViewState
327         var startViewState = obtainViewState(startHostState)
328         startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
329 
330         if (!endHostState.visible) {
331             // Let's handle the case where the end is gone first. In this case we take the
332             // start viewState and will make it gone
333             if (startViewState == null || startHostState == null || !startHostState.visible) {
334                 // the start isn't a valid state, let's use the endstate directly
335                 result = endViewState
336             } else {
337                 // Let's get the gone presentation from the start state
338                 result = layoutController.getGoneState(startViewState,
339                         startHostState.disappearParameters,
340                         transitionProgress,
341                         tmpState)
342             }
343         } else if (startHostState != null && !startHostState.visible) {
344             // We have a start state and it is gone.
345             // Let's get presentation from the endState
346             result = layoutController.getGoneState(endViewState, endHostState.disappearParameters,
347                     1.0f - transitionProgress,
348                     tmpState)
349         } else if (transitionProgress == 1.0f || startViewState == null) {
350             // We're at the end. Let's use that state
351             result = endViewState
352         } else if (transitionProgress == 0.0f) {
353             // We're at the start. Let's use that state
354             result = startViewState
355         } else {
356             result = layoutController.getInterpolatedState(startViewState, endViewState,
357                     transitionProgress, tmpState)
358         }
359         layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
360                 animationDelay)
361     }
362 
363     private fun updateViewStateToCarouselSize(
364         viewState: TransitionViewState?,
365         location: Int,
366         outState: TransitionViewState
367     ) : TransitionViewState? {
368         val result = viewState?.copy(outState) ?: return null
369         val overrideSize = mediaHostStatesManager.carouselSizes[location]
370         overrideSize?.let {
371             // To be safe we're using a maximum here. The override size should always be set
372             // properly though.
373             result.height = Math.max(it.measuredHeight, result.height)
374             result.width = Math.max(it.measuredWidth, result.width)
375         }
376         return result
377     }
378 
379     /**
380      * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation].
381      * In the event of [location] not being visible, [locationWhenHidden] will be used instead.
382      *
383      * @param location Target
384      * @param locationWhenHidden Location that will be used when the target is not
385      * [MediaHost.visible]
386      * @return State require for executing a transition, and also the respective [MediaHost].
387      */
388     private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
389         val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
390         return obtainViewState(mediaHostState)
391     }
392 
393     /**
394      * Notify that the location is changing right now and a [setCurrentState] change is imminent.
395      * This updates the width the view will me measured with.
396      */
397     fun onLocationPreChange(@MediaLocation newLocation: Int) {
398         obtainViewStateForLocation(newLocation)?.let {
399             layoutController.setMeasureState(it)
400         }
401     }
402 
403     /**
404      * Request that the next state change should be animated with the given parameters.
405      */
406     fun animatePendingStateChange(duration: Long, delay: Long) {
407         animateNextStateChange = true
408         animationDuration = duration
409         animationDelay = delay
410     }
411 
412     /**
413      * Clear all existing measurements and refresh the state to match the view.
414      */
415     fun refreshState() {
416         // Let's clear all of our measurements and recreate them!
417         viewStates.clear()
418         if (firstRefresh) {
419             // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
420             // We'll just load these on demand.
421             ensureAllMeasurements()
422             firstRefresh = false
423         }
424         setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
425                 applyImmediately = true)
426     }
427 }
428 
429 /**
430  * An internal key for the cache of mediaViewStates. This is a subset of the full host state.
431  */
432 private data class CacheKey(
433     var widthMeasureSpec: Int = -1,
434     var heightMeasureSpec: Int = -1,
435     var expansion: Float = 0.0f
436 )
437