Media views now dynamically transition between media hosts

Let there be hosts and transitions.
This also enables us to build again after the rebase

Bug: 154137987
Test: atest SystemUITests
Change-Id: I8aaed1718b35be46abae0b0255793f37b8a1fbd2
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index ae7860e..b87a1db 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -39,7 +39,7 @@
     fun attach(mediaView: MediaHeaderView) {
         view = mediaView
         mediaHost.visibleChangedListener = ::updateVisibility
-        mediaHost.isExpanded = false
+        mediaHost.expansion = 0.0f
         mediaHost.showsOnlyActiveMedia = true
         mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
         mediaView.setMediaHost(mediaHost.hostView)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 0297c90..673fafa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -18,32 +18,23 @@
 
 import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
 
-import android.annotation.LayoutRes;
 import android.app.PendingIntent;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.ImageDecoder;
-import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.RippleDrawable;
-import android.media.MediaDescription;
 import android.media.MediaMetadata;
-import android.media.ThumbnailUtils;
 import android.media.session.MediaController;
 import android.media.session.MediaController.PlaybackInfo;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
-import android.net.Uri;
 import android.service.media.MediaBrowserService;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -56,10 +47,11 @@
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
+import androidx.constraintlayout.motion.widget.Key;
+import androidx.constraintlayout.motion.widget.KeyAttributes;
+import androidx.constraintlayout.motion.widget.KeyFrames;
 import androidx.constraintlayout.motion.widget.MotionLayout;
 import androidx.constraintlayout.widget.ConstraintSet;
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
 
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
@@ -73,7 +65,7 @@
 
 import org.jetbrains.annotations.NotNull;
 
-import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -100,7 +92,8 @@
     private final ActivityStarter mActivityStarter;
 
     private Context mContext;
-    protected MotionLayout mMediaNotifView;
+    private MotionLayout mMediaNotifView;
+    private final View mBackground;
     private View mSeamless;
     private MediaSession.Token mToken;
     private MediaController mController;
@@ -109,6 +102,7 @@
     private MediaDevice mDevice;
     protected ComponentName mServiceComponent;
     private boolean mIsRegistered = false;
+    private final List<KeyFrames> mKeyFrames;
     private String mKey;
 
     public static final String MEDIA_PREFERENCES = "media_control_prefs";
@@ -184,8 +178,6 @@
      * @param context
      * @param parent
      * @param routeManager Manager used to listen for device change events.
-     * @param layoutId layout resource to use for this control panel
-     * @param actionIds resource IDs for action buttons in the layout
      * @param foregroundExecutor foreground executor
      * @param backgroundExecutor background executor, used for processing artwork
      * @param activityStarter activity starter
@@ -196,6 +188,8 @@
         mContext = context;
         LayoutInflater inflater = LayoutInflater.from(mContext);
         mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
+        mBackground = mMediaNotifView.findViewById(R.id.media_background);
+        mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
         // TODO(b/150854549): removeOnAttachStateChangeListener when this doesn't inflate views
         // mStateListener shouldn't need to be unregistered since this object shares the same
         // lifecycle with the inflated view. It would be better, however, if this controller used an
@@ -250,7 +244,7 @@
      * Bind this view based on the data given
      */
     public void bind(@NotNull MediaData data) {
-        mToken = data.getToken();
+        MediaSession.Token token = data.getToken();
         mForegroundColor = data.getForegroundColor();
         mBackgroundColor = data.getBackgroundColor();
         if (mToken == null || !mToken.equals(token)) {
@@ -265,7 +259,6 @@
         }
 
         mController = new MediaController(mContext, mToken);
-        mKey = key;
 
         // Try to find a browser service component for this app
         // TODO also check for a media button receiver intended for restarting (b/154127084)
@@ -291,9 +284,8 @@
 
         mController.registerCallback(mSessionCallback);
 
-        albumView.setImageDrawable(data.getArtwork());
-
-        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
+        mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
+                ColorStateList.valueOf(mBackgroundColor));
 
         // Click action
         PendingIntent clickIntent = data.getClickIntent();
@@ -303,6 +295,9 @@
             });
         }
 
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        albumView.setImageDrawable(data.getArtwork());
+
         // App icon
         ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
         // TODO: look at iconDrawable
@@ -310,14 +305,21 @@
         iconDrawable.setTint(mForegroundColor);
         appIcon.setImageDrawable(iconDrawable);
 
+        // Song name
         TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
         titleText.setText(data.getSong());
+        titleText.setTextColor(data.getForegroundColor());
+
+        // App title
         TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
         appName.setText(data.getApp());
         appName.setTextColor(mForegroundColor);
+
+        // Artist name
         TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
         artistText.setText(data.getArtist());
         artistText.setTextColor(mForegroundColor);
+
         // Transfer chip
         mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
         if (mSeamless != null) {
@@ -352,15 +354,16 @@
         int i = 0;
         List<MediaAction> actionIcons = data.getActions();
         for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
-            final ImageButton button = mMediaNotifView.findViewById(ACTION_IDS[i]);
+            int actionId = ACTION_IDS[i];
+            final ImageButton button = mMediaNotifView.findViewById(actionId);
             MediaAction mediaAction = actionIcons.get(i);
             button.setImageDrawable(mediaAction.getDrawable());
             button.setContentDescription(mediaAction.getContentDescription());
             button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
             PendingIntent actionIntent = mediaAction.getIntent();
 
-            if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) {
-                ((IlluminationDrawable) mMediaNotifView.getBackground())
+            if (mBackground.getBackground() instanceof IlluminationDrawable) {
+                ((IlluminationDrawable) mBackground.getBackground())
                         .setupTouch(button, mMediaNotifView);
             }
 
@@ -374,9 +377,10 @@
                 }
             });
             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
-            collapsedSet.setVisibility(ACTION_IDS[i],
+            updateKeyFrameVisibility(actionId, visibleInCompat);
+            collapsedSet.setVisibility(actionId,
                     visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
-            expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.VISIBLE);
+            expandedSet.setVisibility(actionId, ConstraintSet.VISIBLE);
         }
 
         // Hide any unused buttons
@@ -394,41 +398,26 @@
         // TODO: b/156036025 bring back media guts
 
         makeActive();
+    }
 
-        // App title (not in mini player)
-        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
-        if (appName != null) {
-            appName.setText(appNameString);
-            appName.setTextColor(mForegroundColor);
-        }
-
-        // Can be null!
-        MediaMetadata mediaMetadata = mController.getMetadata();
-
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
-        if (albumView != null) {
-            // Resize art in a background thread
-            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView));
-        }
-
-        // Song name
-        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
-        String songName = "";
-        if (mediaMetadata != null) {
-            songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        }
-        titleText.setText(songName);
-        titleText.setTextColor(mForegroundColor);
-
-        // Artist name (not in mini player)
-        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
-        if (artistText != null) {
-            String artistName = "";
-            if (mediaMetadata != null) {
-                artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+    /**
+     * Updates the keyframe visibility such that only views that are not visible actually go
+     * through a transition and fade in.
+     *
+     * @param actionId the id to change
+     * @param visible is the view visible
+     */
+    private void updateKeyFrameVisibility(int actionId, boolean visible) {
+        for (int i = 0; i < mKeyFrames.size(); i++) {
+            KeyFrames keyframe = mKeyFrames.get(i);
+            ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
+            for (int j = 0; j < viewKeyFrames.size(); j++) {
+                Key key = viewKeyFrames.get(j);
+                if (key instanceof KeyAttributes) {
+                    KeyAttributes attributes = (KeyAttributes) key;
+                    attributes.setValue("alpha", visible ? 1.0f : 0.0f);
+                }
             }
-            artistText.setText(artistName);
-            artistText.setTextColor(mForegroundColor);
         }
     }
 
@@ -502,120 +491,6 @@
     }
 
     /**
-     * Process album art for layout
-     * @param description media description
-     * @param albumView view to hold the album art
-     */
-    protected void processAlbumArt(MediaDescription description, ImageView albumView) {
-        Bitmap albumArt = null;
-
-        // First try loading from URI
-        albumArt = loadBitmapFromUri(description.getIconUri());
-
-        // Then check bitmap
-        if (albumArt == null) {
-            albumArt = description.getIconBitmap();
-        }
-
-        processAlbumArtInternal(albumArt, albumView);
-    }
-
-    /**
-     * Process album art for layout
-     * @param metadata media metadata
-     * @param largeIcon from notification, checked as a fallback if metadata does not have art
-     * @param albumView view to hold the album art
-     */
-    private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) {
-        Bitmap albumArt = null;
-
-        if (metadata != null) {
-            // First look in URI fields
-            for (String field : ART_URIS) {
-                String uriString = metadata.getString(field);
-                if (!TextUtils.isEmpty(uriString)) {
-                    albumArt = loadBitmapFromUri(Uri.parse(uriString));
-                    if (albumArt != null) {
-                        Log.d(TAG, "loaded art from " + field);
-                        break;
-                    }
-                }
-            }
-
-            // Then check bitmap field
-            if (albumArt == null) {
-                albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
-            }
-        }
-
-        // Finally try the notification's largeIcon
-        if (albumArt == null && largeIcon != null) {
-            albumArt = largeIcon.getBitmap();
-        }
-
-        processAlbumArtInternal(albumArt, albumView);
-    }
-
-    /**
-     * Load a bitmap from a URI
-     * @param uri
-     * @return bitmap, or null if couldn't be loaded
-     */
-    private Bitmap loadBitmapFromUri(Uri uri) {
-        // ImageDecoder requires a scheme of the following types
-        if (uri.getScheme() == null) {
-            return null;
-        }
-
-        if (!uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)
-                && !uri.getScheme().equals(ContentResolver.SCHEME_ANDROID_RESOURCE)
-                && !uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
-            return null;
-        }
-
-        ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri);
-        try {
-            return ImageDecoder.decodeBitmap(source);
-        } catch (IOException e) {
-            e.printStackTrace();
-            return null;
-        }
-    }
-
-    /**
-     * Resize and crop the image if provided and update the control view
-     * @param albumArt Bitmap of art to display, or null to hide view
-     * @param albumView View that will hold the art
-     */
-    private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) {
-        // Resize
-        RoundedBitmapDrawable roundedDrawable = null;
-        if (albumArt != null) {
-            float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
-            Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
-            int albumSize = (int) mContext.getResources().getDimension(
-                    R.dimen.qs_media_album_size);
-            Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize);
-            roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
-            roundedDrawable.setCornerRadius(radius);
-        } else {
-            Log.e(TAG, "No album art available");
-        }
-
-        // Now that it's resized, update the UI
-        final RoundedBitmapDrawable result = roundedDrawable;
-        mForegroundExecutor.execute(() -> {
-            if (result != null) {
-                albumView.setImageDrawable(result);
-                albumView.setVisibility(View.VISIBLE);
-            } else {
-                albumView.setImageDrawable(null);
-                albumView.setVisibility(View.GONE);
-            }
-        });
-    }
-
-    /**
      * Update the current device information
      * @param device device information to display
      */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index c9630f4..2e41ffb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -16,22 +16,32 @@
 
 package com.android.systemui.media
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
 import android.annotation.IntDef
 import android.content.Context
 import android.view.LayoutInflater
+import android.view.View
 import android.view.ViewGroup
+import android.view.ViewGroupOverlay
 import android.widget.LinearLayout
 
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.media.InfoMediaManager
 import com.android.settingslib.media.LocalMediaManager
+import com.android.systemui.Interpolators
 import com.android.systemui.R
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.VisualStabilityManager
+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.concurrency.DelayableExecutor
 import java.util.concurrent.Executor
@@ -49,15 +59,46 @@
     @Background private val backgroundExecutor: DelayableExecutor,
     private val localBluetoothManager: LocalBluetoothManager?,
     private val visualStabilityManager: VisualStabilityManager,
-    private val statusBarStateController: StatusBarStateController,
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
+    private val activityStarter: ActivityStarter,
     mediaManager: MediaDataManager
 ) {
+    private var rootOverlay: ViewGroupOverlay? = null
+    private lateinit var currentState: MediaState
+    private var animationStartState: MediaState? = null
+    private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
+        interpolator = Interpolators.FAST_OUT_SLOW_IN
+        addUpdateListener {
+            updateTargetState()
+            applyState(animationStartState!!.interpolate(targetState!!, animatedFraction))
+        }
+        addListener(object : AnimatorListenerAdapter() {
+            private var cancelled: Boolean = false
+
+            override fun onAnimationCancel(animation: Animator?) {
+                cancelled = true
+            }
+            override fun onAnimationEnd(animation: Animator?) {
+                if (!cancelled) {
+                    applyTargetStateIfNotAnimating()
+                }
+            }
+
+            override fun onAnimationStart(animation: Animator?) {
+                cancelled = false
+            }
+        })
+    }
+    private var targetState: MediaState? = null
     private val mediaCarousel: ViewGroup
     private val mediaContent: ViewGroup
     private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
     private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
     private val visualStabilityCallback = ::reorderAllPlayers
+    private var previousLocation = -1
+    private var desiredLocation = -1
     private var currentAttachmentLocation = -1
 
     var shouldListen = true
@@ -68,6 +109,18 @@
             }
         }
 
+    var qsExpansion: Float = 0.0f
+        set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation()
+                if (getQSTransformationProgress() >= 0) {
+                    updateTargetState()
+                    applyTargetStateIfNotAnimating()
+                }
+            }
+        }
+
     init {
         mediaCarousel = inflateMediaCarousel()
         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
@@ -83,6 +136,11 @@
                 }
             }
         })
+        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
+            override fun onStateChanged(newState: Int) {
+                updateDesiredLocation()
+            }
+        })
     }
 
     private fun inflateMediaCarousel(): ViewGroup {
@@ -112,7 +170,7 @@
                     imm, data.packageName)
 
             existingPlayer = MediaControlPanel(context, mediaContent, routeManager,
-                    foregroundExecutor, backgroundExecutor)
+                    foregroundExecutor, backgroundExecutor, activityStarter)
             mediaPlayers[key] = existingPlayer
             val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                     ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -158,56 +216,209 @@
      * @return the hostView associated with this location
      */
     fun register(mediaObject: MediaHost) : ViewGroup {
-        val viewHost = UniqueObjectHost(context)
+        val viewHost = createUniqueObjectHost()
         mediaObject.hostView = viewHost;
         mediaHosts[mediaObject.location] = mediaObject
-        if (mediaObject.location == currentAttachmentLocation) {
+        if (mediaObject.location == desiredLocation) {
             // In case we are overriding a view that is already visible, make sure we attach it
             // to this new host view in the below call
+            desiredLocation = -1
+        }
+        if (mediaObject.location == currentAttachmentLocation) {
             currentAttachmentLocation = -1
         }
-        updateAttachmentLocation()
+        updateDesiredLocation()
         return viewHost
     }
 
-    private fun updateAttachmentLocation() {
-        var desiredLocation = calculateLocation()
-        if (desiredLocation != currentAttachmentLocation) {
-            val host = mediaHosts[desiredLocation]
-            host?.apply {
-                // Remove the carousel from the old host
-                (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
-
-                // Add it to the new one
-                host.hostView.addView(mediaCarousel)
-
-                // Let's perform a transition
-                var previousHost = if (currentAttachmentLocation < 0) {
-                    null
-                } else {
-                    mediaHosts[currentAttachmentLocation]
+    private fun createUniqueObjectHost(): UniqueObjectHost {
+        val viewHost = UniqueObjectHost(context)
+        viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+            override fun onViewAttachedToWindow(p0: View?) {
+                if (rootOverlay == null) {
+                    rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay)
                 }
-                performTransition(previousHost, host)
-                currentAttachmentLocation = desiredLocation
+                viewHost.removeOnAttachStateChangeListener(this)
+            }
+
+            override fun onViewDetachedFromWindow(p0: View?) {
+            }
+        })
+        return viewHost
+    }
+
+    private fun updateDesiredLocation() {
+        val desiredLocation = calculateLocation()
+        if (desiredLocation != this.desiredLocation) {
+            if (this.desiredLocation >= 0) {
+                previousLocation = this.desiredLocation
+            }
+            val isNewView = this.desiredLocation == -1
+            this.desiredLocation = desiredLocation
+            // Let's perform a transition
+            performTransition(applyImmediately = isNewView)
+        }
+    }
+
+    private fun performTransition(applyImmediately: Boolean) {
+        if (previousLocation < 0 || applyImmediately) {
+            cancelAnimationAndApplyDesiredState()
+            return
+        }
+        val currentHost = getHost(desiredLocation)
+        val previousHost = getHost(previousLocation)
+        if (currentHost == null || previousHost == null) {
+            cancelAnimationAndApplyDesiredState()
+            return;
+        }
+        updateTargetState()
+        if (isCurrentlyInGuidedTransformation()) {
+            applyTargetStateIfNotAnimating()
+        } else if (shouldAnimateTransition(currentHost, previousHost)) {
+            animator.cancel()
+            // Let's animate to the new position, starting from the current position
+            animationStartState = currentState.copy()
+            adjustAnimatorForTransition(previousLocation, desiredLocation)
+            animator.start()
+        } else {
+            cancelAnimationAndApplyDesiredState()
+        }
+    }
+
+    private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean {
+        if (currentHost.location == LOCATION_QQS
+                && previousHost.location == LOCATION_LOCKSCREEN
+                && (statusBarStateController.leaveOpenOnKeyguardHide()
+                        || statusBarStateController.state == StatusBarState.SHADE_LOCKED)) {
+            // Usually listening to the isShown is enough to determine this, but there is some
+            // non-trivial reattaching logic happening that will make the view not-shown earlier
+            return true
+        }
+        return mediaCarousel.isShown || animator.isRunning
+    }
+
+    private fun adjustAnimatorForTransition(previousLocation: Int, desiredLocation: Int) {
+        var animDuration = 200L
+        var delay = 0L
+        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+            // Going to the full shade, let's adjust the animation duration
+            if (statusBarStateController.state == StatusBarState.SHADE
+                    && keyguardStateController.isKeyguardFadingAway) {
+                delay = keyguardStateController.keyguardFadingAwayDelay
+            }
+            animDuration = StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE.toLong()
+        } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
+            animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
+        }
+        animator.apply {
+            duration  = animDuration
+            startDelay = delay
+        }
+
+    }
+
+    private fun applyTargetStateIfNotAnimating() {
+        if (!animator.isRunning) {
+            // Let's immediately apply the target state (which is interpolated) if there is
+            // no animation running. Otherwise the animation update will already update
+            // the location
+            applyState(targetState!!)
+        }
+    }
+
+    private fun updateTargetState() {
+        if (isCurrentlyInGuidedTransformation()) {
+            val progress = getTransformationProgress()
+            val currentHost = getHost(desiredLocation)!!
+            val previousHost = getHost(previousLocation)!!
+            val newState = currentHost.currentState
+            val previousState = previousHost.currentState
+            targetState = previousState.interpolate(newState, progress)
+        } else {
+            targetState = getHost(desiredLocation)?.currentState
+        }
+    }
+
+    /**
+     * @return true if this transformation is guided by an external progress like a finger
+     */
+    private fun isCurrentlyInGuidedTransformation() : Boolean {
+        return getTransformationProgress() >= 0
+    }
+
+    /**
+     * @return the current transformation progress if we're in a guided transformation and -1 
+     * otherwise
+     */
+    private fun getTransformationProgress(): Float {
+        val progress = getQSTransformationProgress()
+        if (progress >= 0) {
+            return progress
+        }
+        return -1.0f
+    }
+
+    private fun getQSTransformationProgress(): Float {
+        val currentHost = getHost(desiredLocation)
+        val previousHost = getHost(previousLocation)
+        if (currentHost?.location == LOCATION_QS) {
+            if (previousHost?.location == LOCATION_QQS) {
+                return qsExpansion
+            }
+        }
+        return -1.0f
+    }
+
+    private fun getHost(@MediaLocation location: Int): MediaHost? {
+        if (location < 0) {
+            return null
+        }
+        return mediaHosts[location]
+    }
+
+    private fun cancelAnimationAndApplyDesiredState() {
+        animator.cancel()
+        getHost(desiredLocation)?.let {
+            applyState(it.currentState)
+        }
+    }
+
+    private fun applyState(state: MediaState) {
+        updateHostAttachment()
+        for (mediaPlayer in mediaPlayers.values) {
+            val view = mediaPlayer.view
+            view.progress = state.expansion
+        }
+        val boundsOnScreen = state.boundsOnScreen
+        if (currentAttachmentLocation == IN_OVERLAY) {
+            mediaCarousel.setLeftTopRightBottom(boundsOnScreen.left, boundsOnScreen.top,
+                    boundsOnScreen.right, boundsOnScreen.bottom)
+        }
+        currentState = state.copy()
+    }
+
+    private fun updateHostAttachment() {
+        val inOverlay = isTransitionRunning() && rootOverlay != null
+        val newLocation = if (inOverlay) IN_OVERLAY else desiredLocation
+        if (currentAttachmentLocation != newLocation) {
+            currentAttachmentLocation = newLocation
+
+            // Remove the carousel from the old host
+            (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
+
+            // Add it to the new one
+            val targetHost = getHost(desiredLocation)!!.hostView
+            if (inOverlay) {
+                rootOverlay!!.add(mediaCarousel)
+            } else {
+                targetHost.addView(mediaCarousel)
             }
         }
     }
 
-    private fun performTransition(previousHost: MediaHost?, newHost: MediaHost) {
-        if (previousHost == null) {
-            applyObjectStateImmediately(newHost)
-            return
-        }
-        // TODO: actually transition!
-        applyObjectStateImmediately(newHost)
-    }
-
-    private fun applyObjectStateImmediately(newObject: MediaHost) {
-        val expansion = if (newObject.isExpanded) 1.0f else 0.0f;
-        for (mediaPlayer in mediaPlayers.values) {
-            val view = mediaPlayer.view
-            view.progress = expansion
-        }
+    private fun isTransitionRunning(): Boolean {
+        return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f
+                || animator.isRunning
     }
 
     @MediaLocation
@@ -216,7 +427,8 @@
                 && (statusBarStateController.state == StatusBarState.KEYGUARD
                 || statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER))
         return when {
-            qsExpansion > 0.0f -> LOCATION_QS
+            qsExpansion > 0.0f && !onLockscreen -> LOCATION_QS
+            qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
             onLockscreen -> LOCATION_LOCKSCREEN
             else -> LOCATION_QQS
         }
@@ -225,12 +437,6 @@
     /**
      * The expansion of quick settings
      */
-    var qsExpansion: Float = 0.0f
-        set(value) {
-            field = value
-            updateAttachmentLocation()
-        }
-
     @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
     @Retention(AnnotationRetention.SOURCE)
     annotation class MediaLocation
@@ -250,5 +456,10 @@
          * Attached on the lock screen
          */
         const val LOCATION_LOCKSCREEN = 2
+
+        /**
+         * Attached at the root of the hierarchy in an overlay
+         */
+        const val IN_OVERLAY = -1000
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 7df61f20..42bf0e8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -1,28 +1,51 @@
 package com.android.systemui.media
 
+import android.graphics.Rect
+import android.util.MathUtils
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
 import android.view.ViewGroup
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.media.InfoMediaManager
-import com.android.settingslib.media.LocalMediaManager
 import com.android.systemui.media.MediaHierarchyManager.MediaLocation
 import javax.inject.Inject
-import javax.inject.Singleton
 
 class MediaHost @Inject constructor(
+    private val state: MediaHostState,
     private val mediaHierarchyManager: MediaHierarchyManager,
     private val mediaDataManager: MediaDataManager
-) {
+) : MediaState by state {
+    lateinit var hostView: ViewGroup
     var location: Int = -1
         private set
-    lateinit var hostView: ViewGroup
-    var isExpanded: Boolean = false
-    var showsOnlyActiveMedia: Boolean = false
     var visibleChangedListener: ((Boolean) -> Unit)? = null
     var visible: Boolean = false
         private set
 
+    private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
+
+    /**
+     * Get the current Media state. This also updates the location on screen
+     */
+    val currentState : MediaState
+        get () {
+            hostView.getLocationOnScreen(tmpLocationOnScreen)
+            var left = tmpLocationOnScreen[0] + hostView.paddingLeft
+            var top = tmpLocationOnScreen[1] + hostView.paddingTop
+            var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
+            var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
+            // Handle cases when the width or height is 0 but it has padding. In those cases
+            // the above could return negative widths, which is wrong
+            if (right < left) {
+                left = 0
+                right = 0;
+            }
+            if (bottom < top) {
+                bottom = 0
+                top = 0;
+            }
+            state.boundsOnScreen.set(left, top, right, bottom)
+            return state
+        }
+
     private val listener = object : MediaDataManager.Listener {
         override fun onMediaDataLoaded(key: String, data: MediaData) {
             updateViewVisibility()
@@ -66,4 +89,42 @@
         hostView.visibility = if (visible) View.VISIBLE else View.GONE
         visibleChangedListener?.invoke(visible)
     }
+
+    class MediaHostState @Inject constructor() : MediaState {
+        override var expansion: Float = 0.0f
+        override var showsOnlyActiveMedia: Boolean = false
+        override val boundsOnScreen: Rect = Rect()
+
+        override fun copy() : MediaState {
+            val mediaHostState = MediaHostState()
+            mediaHostState.expansion = expansion
+            mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
+            mediaHostState.boundsOnScreen.set(boundsOnScreen)
+            return mediaHostState
+        }
+
+        override fun interpolate(other: MediaState, amount: Float) : MediaState {
+            val result = MediaHostState()
+            result.expansion = MathUtils.lerp(expansion, other.expansion, amount)
+            val left = MathUtils.lerp(boundsOnScreen.left.toFloat(),
+                    other.boundsOnScreen.left.toFloat(), amount).toInt()
+            val top = MathUtils.lerp(boundsOnScreen.top.toFloat(),
+                    other.boundsOnScreen.top.toFloat(), amount).toInt()
+            val right = MathUtils.lerp(boundsOnScreen.right.toFloat(),
+                    other.boundsOnScreen.right.toFloat(), amount).toInt()
+            val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(),
+                    other.boundsOnScreen.bottom.toFloat(), amount).toInt()
+            result.boundsOnScreen.set(left, top, right, bottom)
+            result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
+            return  result
+        }
+    }
+}
+
+interface MediaState {
+    var expansion: Float
+    var showsOnlyActiveMedia: Boolean
+    val boundsOnScreen: Rect
+    fun copy() : MediaState
+    fun interpolate(other: MediaState, amount: Float) : MediaState
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 088630c..6b0775f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -167,7 +167,7 @@
         int height = calculateContainerHeight();
         setBottom(getTop() + height);
         mQSDetail.setBottom(getTop() + height);
-        // Pin QS Footer to the bottom of the panel.
+        // Pin the drag handle to the bottom of the panel.
         mDragHandle.setTranslationY(height - mDragHandle.getHeight());
         mBackground.setTop(mQSPanel.getTop());
         mBackground.setBottom(height);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 5b09267..865fd07 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -37,6 +37,7 @@
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 import com.android.systemui.R.id;
+import com.android.systemui.media.MediaHost;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizer;
@@ -47,6 +48,7 @@
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.util.InjectionInflationController;
 import com.android.systemui.util.LifecycleFragment;
+import com.android.systemui.util.Utils;
 
 import javax.inject.Inject;
 
@@ -91,6 +93,7 @@
      */
     private int mState;
     private QSContainerImplController mQSContainerImplController;
+    private int[] mTmpLocation = new int[2];
 
     @Inject
     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
@@ -377,8 +380,7 @@
         mLastKeyguardAndExpanded = onKeyguardAndExpanded;
 
         boolean fullyExpanded = expansion == 1;
-        int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom()
-                + mFooter.getHeight();
+        int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom();
         float panelTranslationY = translationScaleY * heightDiff;
 
         // Let the views animate their contents correctly by giving them the necessary context.
@@ -404,6 +406,32 @@
         if (mQSAnimator != null) {
             mQSAnimator.setPosition(expansion);
         }
+        updateMediaPositions();
+    }
+
+    private void updateMediaPositions() {
+        if (Utils.useQsMediaPlayer(getContext())) {
+            mContainer.getLocationOnScreen(mTmpLocation);
+            float absoluteBottomPosition = mTmpLocation[1] + mContainer.getHeight();
+            pinToBottom(absoluteBottomPosition, mQSPanel.getMediaHost());
+            pinToBottom(absoluteBottomPosition - mHeader.getPaddingBottom(),
+                    mHeader.getHeaderQsPanel().getMediaHost());
+        }
+    }
+
+    private void pinToBottom(float absoluteBottomPosition, MediaHost mediaHost) {
+        View hostView = mediaHost.getHostView();
+        if (mLastQSExpansion > 0) {
+            ViewGroup.MarginLayoutParams params =
+                    (ViewGroup.MarginLayoutParams) hostView.getLayoutParams();
+            float targetPosition = absoluteBottomPosition - params.bottomMargin
+                    - hostView.getHeight();
+            float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top
+                    - hostView.getTranslationY();
+            hostView.setTranslationY(targetPosition - currentPosition);
+        } else {
+            hostView.setTranslationY(0);
+        }
     }
 
     private boolean headerWillBeAnimating() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 0222969..3e87b02 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -104,7 +104,6 @@
     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
     private final QSTileRevealController mQsTileRevealController;
 
-    private ActivityStarter mActivityStarter;
     protected boolean mExpanded;
     protected boolean mListening;
 
@@ -148,7 +147,6 @@
             BroadcastDispatcher broadcastDispatcher,
             QSLogger qsLogger,
             MediaHost mediaHost,
-            ActivityStarter activityStarter,
             UiEventLogger uiEventLogger
     ) {
         super(context, attrs);
@@ -157,7 +155,6 @@
         mQSLogger = qsLogger;
         mDumpManager = dumpManager;
         mBroadcastDispatcher = broadcastDispatcher;
-        mActivityStarter = activityStarter;
         mUiEventLogger = uiEventLogger;
 
         setOrientation(VERTICAL);
@@ -186,7 +183,6 @@
                 findViewById(R.id.brightness_slider), mBroadcastDispatcher);
     }
 
-
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
@@ -198,7 +194,7 @@
 
     protected void addMediaHostView() {
         mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
-        mMediaHost.setExpanded(true);
+        mMediaHost.setExpansion(1.0f);
         mMediaHost.setShowsOnlyActiveMedia(false);
         ViewGroup hostView = mMediaHost.getHostView();
         addView(hostView);
@@ -219,7 +215,8 @@
         @Override
         public void addTrack(MediaDescription desc, ComponentName component,
                 QSMediaBrowser browser) {
-            if (component == null) {
+            // TODO: Fix Resumption b/156104922
+/*            if (component == null) {
                 Log.e(TAG, "Component cannot be null");
                 return;
             }
@@ -255,7 +252,7 @@
             int iconColor = Color.DKGRAY;
             int bgColor = Color.LTGRAY;
             player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(),
-                    pkgName);
+                    pkgName);*/
         }
     };
 
@@ -856,6 +853,10 @@
         }
     }
 
+    public MediaHost getMediaHost() {
+        return mMediaHost;
+    }
+
     private class H extends Handler {
         private static final int SHOW_DETAIL = 1;
         private static final int SET_TILE_VISIBILITY = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 194cd59..dfd385d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -83,11 +83,10 @@
             BroadcastDispatcher broadcastDispatcher,
             QSLogger qsLogger,
             MediaHost mediaHost,
-            ActivityStarter activityStarter,
             UiEventLogger uiEventLogger
     ) {
-        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, 
-                activityStarter, uiEventLogger);
+        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost,
+                uiEventLogger);
         if (mFooter != null) {
             removeView(mFooter.getView());
         }
@@ -193,7 +192,7 @@
             return null;
         });
         mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
-        mMediaHost.setExpanded(false);
+        mMediaHost.setExpansion(0.0f);
         mMediaHost.setShowsOnlyActiveMedia(true);
         reAttachMediaHost();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 7901363..3b2bea8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -220,6 +220,10 @@
         mNextAlarmTextView.setSelected(true);
     }
 
+    public QuickQSPanel getHeaderQsPanel() {
+        return mHeaderQsPanel;
+    }
+
     private List<String> getIgnoredIconSlots() {
         ArrayList<String> ignored = new ArrayList<>();
         ignored.add(mContext.getResources().getString(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 5797944..0831c0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -767,6 +767,10 @@
         return mContentTranslation;
     }
 
+    public boolean wantsAddAndRemoveAnimations() {
+        return true;
+    }
+
     /**
      * A listener notifying when {@link #getActualHeight} changes.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
index 82560f5..65594ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java
@@ -60,4 +60,9 @@
         layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
         layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
     }
+
+    @Override
+    public boolean wantsAddAndRemoveAnimations() {
+        return false;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 7f32c00..db9faf5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -3074,6 +3074,9 @@
      */
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private boolean generateRemoveAnimation(ExpandableView child) {
+        if (!child.wantsAddAndRemoveAnimations()) {
+            return false;
+        }
         if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
             mAddedHeadsUpChildren.remove(child);
             return false;
@@ -3428,7 +3431,8 @@
     @Override
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
-        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) {
+        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()
+                && child.wantsAddAndRemoveAnimations()) {
             // Generate Animations
             mChildrenToAddAnimated.add(child);
             if (fromMoreCard) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index aa6baff..8161a83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -3518,7 +3518,11 @@
             // Calculate quick setting heights.
             int oldMaxHeight = mQsMaxExpansionHeight;
             if (mQs != null) {
+                float previousMin = mQsMinExpansionHeight;
                 mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight();
+                if (mQsExpansionHeight == previousMin) {
+                    mQsExpansionHeight = mQsMinExpansionHeight;
+                }
                 mQsMaxExpansionHeight = mQs.getDesiredHeight();
                 mNotificationStackScroller.setMaxTopPadding(
                         mQsMaxExpansionHeight + mQsNotificationTopPadding);