Measuring the Media Views now properly the first time its created

Instead of only knowing the measurement when its attached, we need
to know before otherwise the layout will be broken

Bug: 154137987
Test: atest SystemUITests
Change-Id: I8a4d90a7d933f477df65a5f26942059b0f0920da
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 0b0bfc6..3d638dd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -112,6 +112,7 @@
     private int mAlbumArtSize;
     private int mAlbumArtRadius;
     private int mViewWidth;
+    private MediaMeasurementInput mLastMeasureInput;
 
     public static final String MEDIA_PREFERENCES = "media_control_prefs";
     public static final String MEDIA_PREFERENCE_KEY = "browser_components";
@@ -737,31 +738,31 @@
      */
     protected void removePlayer() { }
 
-    public void setDimension(int newWidth, int newHeight, boolean animate, long duration,
+    public void remeasure(@Nullable MediaMeasurementInput input, boolean animate, long duration,
             long startDelay) {
         // Let's remeasure if our width changed. Our height is dependent on the expansion, so we
         // won't animate if it changed
-        if (newWidth != mViewWidth) {
+        if (input != null && !input.sameAs(mLastMeasureInput)) {
+            mLastMeasureInput = input;
             if (animate) {
                 mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
             }
-            setViewWidth(newWidth);
-            mMediaNotifView.layout(0, 0, newWidth, mMediaNotifView.getMeasuredHeight());
+            remeasureInternal(input);
+            mMediaNotifView.layout(0, 0, mMediaNotifView.getMeasuredWidth(),
+                    mMediaNotifView.getMeasuredHeight());
         }
     }
 
-    protected void setViewWidth(int newWidth) {
+    public MediaMeasurementInput getLastMeasureInput() {
+        return mLastMeasureInput;
+    }
+
+    private void remeasureInternal(MediaMeasurementInput input) {
         ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
         ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
-        collapsedSet.setGuidelineBegin(R.id.view_width, newWidth);
-        expandedSet.setGuidelineBegin(R.id.view_width, newWidth);
-        DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
-        int widthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
-                View.MeasureSpec.AT_MOST);
-        int heightSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
-                View.MeasureSpec.AT_MOST);
-        mMediaNotifView.setMinimumWidth(displayMetrics.widthPixels);
-        mMediaNotifView.measure(widthSpec, heightSpec);
-        mViewWidth = newWidth;
+        int width = input.getWidth();
+        collapsedSet.setGuidelineBegin(R.id.view_width, width);
+        expandedSet.setGuidelineBegin(R.id.view_width, width);
+        mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec());
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index fc141bf..26a6104 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.animation.UniqueObjectHost
+import com.android.systemui.util.animation.UniqueObjectHostView
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -45,7 +45,8 @@
     private val statusBarStateController: SysuiStatusBarStateController,
     private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
-    private val mediaViewManager: MediaViewManager
+    private val mediaViewManager: MediaViewManager,
+    private val mediaMeasurementProvider: MediaMeasurementManager
 ) {
     private var rootOverlay: ViewGroupOverlay? = null
     private lateinit var currentState: MediaState
@@ -108,7 +109,7 @@
      * @return the hostView associated with this location
      */
     fun register(mediaObject: MediaHost) : ViewGroup {
-        val viewHost = createUniqueObjectHost()
+        val viewHost = createUniqueObjectHost(mediaObject)
         mediaObject.hostView = viewHost;
         mediaHosts[mediaObject.location] = mediaObject
         if (mediaObject.location == desiredLocation) {
@@ -123,8 +124,10 @@
         return viewHost
     }
 
-    private fun createUniqueObjectHost(): UniqueObjectHost {
-        val viewHost = UniqueObjectHost(context)
+    private fun createUniqueObjectHost(host: MediaState): UniqueObjectHostView {
+        val viewHost = UniqueObjectHostView(context)
+        viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host)
+
         viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
             override fun onViewAttachedToWindow(p0: View?) {
                 if (rootOverlay == null) {
@@ -148,12 +151,16 @@
             val isNewView = this.desiredLocation == -1
             this.desiredLocation = desiredLocation
             // Let's perform a transition
-            performTransition(applyImmediately = isNewView)
+            val animate = shouldAnimateTransition(desiredLocation, previousLocation)
+            val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+            mediaViewManager.onDesiredStateChanged(getHost(desiredLocation)?.currentState,
+                    animate, animDuration, delay)
+            performTransitionToNewLocation(isNewView, animate)
         }
     }
 
-    private fun performTransition(applyImmediately: Boolean) {
-        if (previousLocation < 0 || applyImmediately) {
+    private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) {
+        if (previousLocation < 0 || isNewView) {
             cancelAnimationAndApplyDesiredState()
             return
         }
@@ -161,29 +168,27 @@
         val previousHost = getHost(previousLocation)
         if (currentHost == null || previousHost == null) {
             cancelAnimationAndApplyDesiredState()
-            return;
         }
         updateTargetState()
-        var animate = false
         if (isCurrentlyInGuidedTransformation()) {
             applyTargetStateIfNotAnimating()
-        } else if (shouldAnimateTransition(currentHost, previousHost)) {
+        } else if (animate) {
             animator.cancel()
             // Let's animate to the new position, starting from the current position
             animationStartState = currentState.copy()
-            adjustAnimatorForTransition(previousLocation, desiredLocation)
+            adjustAnimatorForTransition(desiredLocation, previousLocation)
             animator.start()
-            animate = true
         } else {
             cancelAnimationAndApplyDesiredState()
         }
-        mediaViewManager.performTransition(targetState, animate, animator.duration,
-                animator.startDelay)
     }
 
-    private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean {
-        if (currentHost.location == LOCATION_QQS
-                && previousHost.location == LOCATION_LOCKSCREEN
+    private fun shouldAnimateTransition(
+        @MediaLocation currentLocation: Int,
+        @MediaLocation previousLocation: Int
+    ): Boolean {
+        if (currentLocation == LOCATION_QQS
+                && previousLocation == LOCATION_LOCKSCREEN
                 && (statusBarStateController.leaveOpenOnKeyguardHide()
                         || statusBarStateController.state == StatusBarState.SHADE_LOCKED)) {
             // Usually listening to the isShown is enough to determine this, but there is some
@@ -193,7 +198,16 @@
         return mediaCarousel.isShown || animator.isRunning
     }
 
-    private fun adjustAnimatorForTransition(previousLocation: Int, desiredLocation: Int) {
+    private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
+        val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+        animator.apply {
+            duration  = animDuration
+            startDelay = delay
+        }
+
+    }
+
+    private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
         var animDuration = 200L
         var delay = 0L
         if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
@@ -206,11 +220,7 @@
         } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
             animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
         }
-        animator.apply {
-            duration  = animDuration
-            startDelay = delay
-        }
-
+        return animDuration to delay
     }
 
     private fun applyTargetStateIfNotAnimating() {
@@ -290,7 +300,7 @@
                     boundsOnScreen.bottom)
         }
         currentState = state.copy()
-        mediaViewManager.applyState(currentState)
+        mediaViewManager.setCurrentState(currentState)
     }
 
     private fun updateHostAttachment() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 12e2743..80d00e2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -6,6 +6,8 @@
 import android.view.View.OnAttachStateChangeListener
 import android.view.ViewGroup
 import com.android.systemui.media.MediaHierarchyManager.MediaLocation
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementInputData
 import javax.inject.Inject
 
 class MediaHost @Inject constructor(
@@ -86,6 +88,7 @@
     }
 
     class MediaHostState @Inject constructor() : MediaState {
+        var measurementInput: MediaMeasurementInput? = null
         override var expansion: Float = 0.0f
         override var showsOnlyActiveMedia: Boolean = false
         override val boundsOnScreen: Rect = Rect()
@@ -95,6 +98,7 @@
             mediaHostState.expansion = expansion
             mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
             mediaHostState.boundsOnScreen.set(boundsOnScreen)
+            mediaHostState.measurementInput = measurementInput
             return mediaHostState
         }
 
@@ -111,8 +115,20 @@
                     other.boundsOnScreen.bottom.toFloat(), amount).toInt()
             result.boundsOnScreen.set(left, top, right, bottom)
             result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
+            if (amount > 0.0f) {
+                if (other is MediaHostState) {
+                    result.measurementInput = other.measurementInput
+                }
+            }  else {
+                result.measurementInput
+            }
             return  result
         }
+
+        override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput {
+            measurementInput = MediaMeasurementInput(input, expansion)
+            return measurementInput as MediaMeasurementInput
+        }
     }
 }
 
@@ -122,4 +138,20 @@
     val boundsOnScreen: Rect
     fun copy() : MediaState
     fun interpolate(other: MediaState, amount: Float) : MediaState
-}
\ No newline at end of file
+    fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput
+}
+/**
+ * The measurement input for a Media View
+ */
+data class MediaMeasurementInput(
+    private val viewInput: MeasurementInput,
+    val expansion: Float) : MeasurementInput by viewInput {
+
+    override fun sameAs(input: MeasurementInput?): Boolean {
+        if (!(input is MediaMeasurementInput)) {
+            return false
+        }
+        return width == input.width && expansion == input.expansion
+    }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
new file mode 100644
index 0000000..4bbf5eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.media
+
+import com.android.systemui.util.animation.BaseMeasurementCache
+import com.android.systemui.util.animation.GuaranteedMeasurementCache
+import com.android.systemui.util.animation.MeasurementCache
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementOutput
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * A class responsible creating measurement caches for media hosts which also coordinates with
+ * the view manager to obtain sizes for unknown measurement inputs.
+ */
+@Singleton
+class MediaMeasurementManager @Inject constructor(
+    private val mediaViewManager: MediaViewManager
+) {
+    private val baseCache: MeasurementCache
+
+    init {
+        baseCache = BaseMeasurementCache()
+    }
+
+    private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
+        return mediaViewManager.obtainMeasurement(input)
+    }
+
+    /**
+     * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that
+     * requesting any size from the cache will always return the correct value.
+     */
+    fun obtainCache(host: MediaState): GuaranteedMeasurementCache {
+        val remapper = { input: MeasurementInput ->
+            host.getMeasuringInput(input)
+        }
+        val provider = { input: MeasurementInput ->
+            provideMeasurement(input as MediaMeasurementInput)
+        }
+        return GuaranteedMeasurementCache(baseCache, remapper, provider)
+    }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 636e4e4..6dd9787 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -4,7 +4,6 @@
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import android.widget.LinearLayout
-
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.media.InfoMediaManager
 import com.android.settingslib.media.LocalMediaManager
@@ -13,7 +12,8 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.notification.VisualStabilityManager
-import com.android.systemui.util.animation.UniqueObjectHost
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.concurrency.DelayableExecutor
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -33,12 +33,14 @@
     private val activityStarter: ActivityStarter,
     mediaManager: MediaDataManager
 ) {
+    private var desiredState: MediaHost.MediaHostState? = null
+    private var currentState: MediaState? = null
     val mediaCarousel: ViewGroup
     private val mediaContent: ViewGroup
     private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
     private val visualStabilityCallback = ::reorderAllPlayers
 
-    private var viewsExpanded = true
+    private var currentlyExpanded = true
         set(value) {
             if (field != value) {
                 field = value
@@ -68,7 +70,7 @@
 
     private fun inflateMediaCarousel(): ViewGroup {
         return LayoutInflater.from(context).inflate(
-                R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup
+                R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
     }
 
     private fun reorderAllPlayers() {
@@ -98,7 +100,7 @@
             val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                     ViewGroup.LayoutParams.WRAP_CONTENT)
             existingPlayer.view.setLayoutParams(lp)
-            existingPlayer.setListening(viewsExpanded)
+            existingPlayer.setListening(currentlyExpanded)
             if (existingPlayer.isPlaying) {
                 mediaContent.addView(existingPlayer.view, 0)
             } else {
@@ -132,32 +134,48 @@
 
     }
 
-    fun applyState(state: MediaState) {
+    fun setCurrentState(state: MediaState) {
+        currentState = state
+        currentlyExpanded = state.expansion > 0
         for (mediaPlayer in mediaPlayers.values) {
             val view = mediaPlayer.view
             view.progress = state.expansion
         }
-        viewsExpanded = state.expansion > 0;
     }
 
     /**
      * @param targetState the target state we're transitioning to
      * @param animate should this be animated
      */
-    fun performTransition(targetState: MediaState?, animate: Boolean, duration: Long,
-                          startDelay: Long) {
-        if (targetState == null) {
-            return
+    fun onDesiredStateChanged(targetState: MediaState?, animate: Boolean, duration: Long,
+                              startDelay: Long) {
+        if (targetState is MediaHost.MediaHostState) {
+            // This is a hosting view, let's remeasure our players
+            desiredState = targetState
+            val measurementInput = targetState.measurementInput
+            for (mediaPlayer in mediaPlayers.values) {
+                mediaPlayer.remeasure(measurementInput, animate, duration, startDelay)
+            }
         }
-        val newWidth = targetState.boundsOnScreen.width()
-        val newHeight = targetState.boundsOnScreen.height()
-        remeasureViews(newWidth, newHeight, animate, duration, startDelay)
     }
 
-    private fun remeasureViews(newWidth: Int, newHeight: Int, animate: Boolean, duration: Long,
-                               startDelay: Long) {
-        for (mediaPlayer in mediaPlayers.values) {
-            mediaPlayer.setDimension(newWidth, newHeight, animate, duration, startDelay)
+    /**
+     * Get a measurement for the given input state. This measures the first player and returns
+     * its bounds as if it were measured with the given measurement dimensions
+     */
+    fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
+        val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
+        // Let's measure the size of the first player and return its height
+        val previousProgress = firstPlayer.view.progress
+        firstPlayer.view.progress = input.expansion
+        firstPlayer.remeasure(input, false /* animate */, 0, 0)
+        val result = MeasurementOutput(firstPlayer.view.measuredWidth,
+                firstPlayer.view.measuredHeight)
+        firstPlayer.view.progress = previousProgress
+        if (desiredState != null) {
+            // remeasure it to the old size again!
+            firstPlayer.remeasure(desiredState!!.measurementInput, false, 0, 0)
         }
+        return result
     }
 }
\ No newline at end of file