Refactored the Media Player management

Previously all media instances had their own management
and it could easily happen that certain players
would get out of sync as a result. We now have
a unified architecture to listen to media notifications
and inflating a singular UI

Test: atest SystemUiTests
Bug: 154137987
Change-Id: I9f807e6431dd7cb54ca9b6d983379d770a281f31
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 96494cf..3a37c0f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -451,7 +451,8 @@
      * @param metadata New metadata.
      */
     @Override
-    public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) {
+    public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata,
+            @PlaybackState.State int state) {
         synchronized (this) {
             boolean nextVisible = NotificationMediaManager.isPlayingState(state);
             mMediaHandler.removeCallbacksAndMessages(null);
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
new file mode 100644
index 0000000..f60c012
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -0,0 +1,67 @@
+/*
+ * 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 android.view.View
+import com.android.internal.util.ContrastColorUtil
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.notification.stack.MediaHeaderView
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * A class that controls the media notifications on the lock screen, handles its visibility and
+ * is responsible for the embedding of he media experience.
+ */
+@Singleton
+class KeyguardMediaController @Inject constructor(
+    private val mediaHierarchyManager: MediaHierarchyManager,
+    private val notifMediaManager: NotificationMediaManager,
+    private val bypassController: KeyguardBypassController
+) {
+    private var view: MediaHeaderView? = null
+
+    init {
+        notifMediaManager.addCallback(object : NotificationMediaManager.MediaListener {
+            override fun onMediaDataLoaded(key: String, data: MediaData) {
+                updateVisibility()
+            }
+
+            override fun onMediaDataRemoved(key: String) {
+                updateVisibility()
+            }
+        })
+    }
+
+    /**
+     * Attach this controller to a media view, initializing its state
+     */
+    fun attach(mediaControlsView: MediaHeaderView) {
+        view = mediaControlsView
+        val hostView = mediaHierarchyManager.createMediaHost(
+                MediaHierarchyManager.LOCATION_LOCKSCREEN)
+        mediaControlsView.setMediaHost(hostView)
+        updateVisibility()
+    }
+
+    private fun updateVisibility() {
+        val shouldBeVisible = notifMediaManager.hasActiveMedia() && !bypassController.bypassEnabled
+        view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE
+    }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 9adfe7d..0297c90 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media;
 
+import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
+
 import android.annotation.LayoutRes;
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -28,9 +30,9 @@
 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.Icon;
 import android.graphics.drawable.RippleDrawable;
 import android.media.MediaDescription;
 import android.media.MediaMetadata;
@@ -50,10 +52,12 @@
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.SeekBar;
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 import androidx.constraintlayout.motion.widget.MotionLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
 
@@ -65,6 +69,9 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSMediaBrowser;
 import com.android.systemui.util.Assert;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+
+import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
 import java.util.List;
@@ -76,6 +83,18 @@
 public class MediaControlPanel {
     private static final String TAG = "MediaControlPanel";
     @Nullable private final LocalMediaManager mLocalMediaManager;
+
+    // Button IDs for QS controls
+    static final int[] ACTION_IDS = {
+            R.id.action0,
+            R.id.action1,
+            R.id.action2,
+            R.id.action3,
+            R.id.action4
+    };
+
+    private final SeekBarViewModel mSeekBarViewModel;
+    private final SeekBarObserver mSeekBarObserver;
     private final Executor mForegroundExecutor;
     protected final Executor mBackgroundExecutor;
     private final ActivityStarter mActivityStarter;
@@ -92,8 +111,6 @@
     private boolean mIsRegistered = false;
     private String mKey;
 
-    private final int[] mActionIds;
-
     public static final String MEDIA_PREFERENCES = "media_control_prefs";
     public static final String MEDIA_PREFERENCE_KEY = "browser_components";
     private SharedPreferences mSharedPrefs;
@@ -101,15 +118,6 @@
     private boolean mIsRemotePlayback;
     private QSMediaBrowser mQSMediaBrowser;
 
-    // Button IDs used in notifications
-    protected static final int[] NOTIF_ACTION_IDS = {
-            com.android.internal.R.id.action0,
-            com.android.internal.R.id.action1,
-            com.android.internal.R.id.action2,
-            com.android.internal.R.id.action3,
-            com.android.internal.R.id.action4
-    };
-
     // URI fields to try loading album art from
     private static final String[] ART_URIS = {
             MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
@@ -183,12 +191,11 @@
      * @param activityStarter activity starter
      */
     public MediaControlPanel(Context context, ViewGroup parent,
-            @Nullable LocalMediaManager routeManager, @LayoutRes int layoutId, int[] actionIds,
-            Executor foregroundExecutor, Executor backgroundExecutor,
-            ActivityStarter activityStarter) {
+            @Nullable LocalMediaManager routeManager, Executor foregroundExecutor,
+            DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
         mContext = context;
         LayoutInflater inflater = LayoutInflater.from(mContext);
-        mMediaNotifView = (MotionLayout) inflater.inflate(layoutId, parent, false);
+        mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
         // 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
@@ -196,21 +203,42 @@
         // mStateListener to be unregistered in detach.
         mMediaNotifView.addOnAttachStateChangeListener(mStateListener);
         mLocalMediaManager = routeManager;
-        mActionIds = actionIds;
         mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mActivityStarter = activityStarter;
+        mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
+        mSeekBarObserver = new SeekBarObserver(getView());
+        // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
+        // priority of players. As soon as it is removed, the lifecycle will end and the seek bar
+        // will stop updating. So, use the lifecycle of the parent instead.
+        // TODO: this parent is also detached, need to fix that
+        mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
+        SeekBar bar = getView().findViewById(R.id.media_progress_bar);
+        bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
+        bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
     }
 
     /**
      * Get the view used to display media controls
      * @return the view
      */
-    public View getView() {
+    public MotionLayout getView() {
         return mMediaNotifView;
     }
 
     /**
+     * Sets the listening state of the player.
+     *
+     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
+     * unnecessary work when the QS panel is closed.
+     *
+     * @param listening True when player should be active. Otherwise, false.
+     */
+    public void setListening(boolean listening) {
+        mSeekBarViewModel.setListening(listening);
+    }
+
+    /**
      * Get the context
      * @return context
      */
@@ -219,20 +247,12 @@
     }
 
     /**
-     * Update the media panel view for the given media session
-     * @param token
-     * @param iconDrawable
-     * @param largeIcon
-     * @param iconColor
-     * @param bgColor
-     * @param contentIntent
-     * @param appNameString
-     * @param key
+     * Bind this view based on the data given
      */
-    public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon,
-            int iconColor, int bgColor, PendingIntent contentIntent, String appNameString,
-            String key) {
-        // Ensure that component names are updated if token has changed
+    public void bind(@NotNull MediaData data) {
+        mToken = data.getToken();
+        mForegroundColor = data.getForegroundColor();
+        mBackgroundColor = data.getBackgroundColor();
         if (mToken == null || !mToken.equals(token)) {
             if (mQSMediaBrowser != null) {
                 Log.d(TAG, "Disconnecting old media browser");
@@ -244,8 +264,6 @@
             mCheckedForResumption = false;
         }
 
-        mForegroundColor = iconColor;
-        mBackgroundColor = bgColor;
         mController = new MediaController(mContext, mToken);
         mKey = key;
 
@@ -258,6 +276,7 @@
             PackageManager pm = mContext.getPackageManager();
             Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
             List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
+            // TODO: look into this resumption
             if (resumeInfo != null) {
                 for (ResolveInfo inf : resumeInfo) {
                     if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
@@ -272,20 +291,33 @@
 
         mController.registerCallback(mSessionCallback);
 
+        albumView.setImageDrawable(data.getArtwork());
+
         mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
 
         // Click action
-        if (contentIntent != null) {
+        PendingIntent clickIntent = data.getClickIntent();
+        if (clickIntent != null) {
             mMediaNotifView.setOnClickListener(v -> {
-                mActivityStarter.postStartActivityDismissingKeyguard(contentIntent);
+                mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
             });
         }
 
         // App icon
-        ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
+        ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
+        // TODO: look at iconDrawable
+        Drawable iconDrawable = data.getAppIcon();
         iconDrawable.setTint(mForegroundColor);
         appIcon.setImageDrawable(iconDrawable);
 
+        TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
+        titleText.setText(data.getSong());
+        TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
+        appName.setText(data.getApp());
+        appName.setTextColor(mForegroundColor);
+        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) {
@@ -313,6 +345,54 @@
             mIsRemotePlayback = false;
         }
 
+        ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
+        ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
+        List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
+        // Media controls
+        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]);
+            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())
+                        .setupTouch(button, mMediaNotifView);
+            }
+
+            button.setOnClickListener(v -> {
+                if (actionIntent != null) {
+                    try {
+                        actionIntent.send();
+                    } catch (PendingIntent.CanceledException e) {
+                        e.printStackTrace();
+                    }
+                }
+            });
+            boolean visibleInCompat = actionsWhenCollapsed.contains(i);
+            collapsedSet.setVisibility(ACTION_IDS[i],
+                    visibleInCompat ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
+            expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.VISIBLE);
+        }
+
+        // Hide any unused buttons
+        for (; i < ACTION_IDS.length; i++) {
+            expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
+            collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
+        }
+
+        // Seek Bar
+        final MediaController controller = new MediaController(getContext(), data.getToken());
+        mBackgroundExecutor.execute(
+                () -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
+
+        // Set up long press menu
+        // TODO: b/156036025 bring back media guts
+
         makeActive();
 
         // App title (not in mini player)
@@ -614,15 +694,16 @@
      */
     protected void resetButtons() {
         // Hide all the old buttons
-        for (int i = 0; i < mActionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
-            if (thisBtn != null) {
-                thisBtn.setVisibility(View.GONE);
-            }
+
+        ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
+        ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
+        for (int i = 1; i < ACTION_IDS.length; i++) {
+            expandedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
+            collapsedSet.setVisibility(ACTION_IDS[i], ConstraintSet.GONE);
         }
 
         // Add a restart button
-        ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
+        ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
         btn.setOnClickListener(v -> {
             Log.d(TAG, "Attempting to restart session");
             if (mQSMediaBrowser != null) {
@@ -644,7 +725,20 @@
         });
         btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
         btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
-        btn.setVisibility(View.VISIBLE);
+        expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
+        collapsedSet.setVisibility(ACTION_IDS[0], ConstraintSet.VISIBLE);
+
+        mSeekBarViewModel.clearController();
+        // TODO: fix guts
+        //        View guts = mMediaNotifView.findViewById(R.id.media_guts);
+        View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
+
+        mMediaNotifView.setOnLongClickListener(v -> {
+            // Replace player view with close/cancel view
+//            guts.setVisibility(View.GONE);
+            options.setVisibility(View.VISIBLE);
+            return true; // consumed click
+        });
     }
 
     private void makeActive() {
@@ -668,7 +762,6 @@
             mIsRegistered = false;
         }
     }
-
     /**
      * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
      * component to the list of resumption components
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
new file mode 100644
index 0000000..8165362
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -0,0 +1,45 @@
+/*
+ * 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 android.app.PendingIntent
+import android.graphics.drawable.Drawable
+import android.media.session.MediaSession
+
+/** State of a media view. */
+data class MediaData(
+    val initialized: Boolean = false,
+    val foregroundColor: Int,
+    val backgroundColor: Int,
+    val app: String?,
+    val appIcon: Drawable?,
+    val artist: String?,
+    val song: String?,
+    val artwork: Drawable?,
+    val actions: List<MediaAction>,
+    val actionsToShowInCompact: List<Int>,
+    val packageName: String?,
+    val token: MediaSession.Token?,
+    val clickIntent: PendingIntent?
+)
+
+/** State of a media action. */
+data class MediaAction(
+    val drawable: Drawable?,
+    val intent: PendingIntent?,
+    val contentDescription: CharSequence?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
new file mode 100644
index 0000000..298a5fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -0,0 +1,206 @@
+/*
+ * 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 android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.media.MediaMetadata
+import android.media.session.MediaSession
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import androidx.core.graphics.drawable.RoundedBitmapDrawable
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
+import com.android.internal.util.ContrastColorUtil
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.notification.MediaNotificationProcessor
+import java.util.*
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.collections.LinkedHashMap
+
+/**
+ * A class that facilitates management and loading of Media Data, ready for binding.
+ */
+@Singleton
+class MediaDataManager @Inject constructor(
+    private val context: Context,
+    private val mediaControllerFactory: MediaControllerFactory,
+    @Background private val backgroundExecutor: Executor,
+    @Main private val foregroundExcecutor: Executor
+) {
+
+    lateinit var listener: NotificationMediaManager.MediaListener
+    private var albumArtSize: Int = 0
+    private var albumArtRadius: Int = 0
+    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+
+    init {
+        loadDimens()
+    }
+
+    private fun loadDimens() {
+        albumArtRadius = context.resources.getDimensionPixelSize(R.dimen.qs_media_corner_radius)
+        albumArtSize = context.resources.getDimensionPixelSize(R.dimen.qs_media_album_size)
+    }
+
+    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+        if (isMediaNotification(sbn)) {
+            if (!mediaEntries.containsKey(key)) {
+                mediaEntries.put(key, LOADING)
+            }
+            loadMediaData(key, sbn)
+        } else {
+            onNotificationRemoved(key)
+        }
+    }
+
+    private fun loadMediaData(key: String, sbn: StatusBarNotification) {
+        backgroundExecutor.execute {
+            loadMediaDataInBg(key, sbn)
+        }
+    }
+
+    private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
+        val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
+                as MediaSession.Token?
+        val metadata = mediaControllerFactory.create(token).metadata
+
+        if (metadata == null) {
+            // TODO: handle this better, removing media notification
+            return
+        }
+
+        // Foreground and Background colors computed from album art
+        val notif: Notification = sbn.notification
+        var fgColor = notif.color
+        var bgColor = -1
+        var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
+        }
+        // TODO: load media data from uri
+        if (artworkBitmap != null) {
+            // If we have art, get colors from that
+            val p = MediaNotificationProcessor.generateArtworkPaletteBuilder(artworkBitmap)
+                    .generate()
+            val swatch = MediaNotificationProcessor.findBackgroundSwatch(p)
+            bgColor = swatch.rgb
+            fgColor = MediaNotificationProcessor.selectForegroundColor(bgColor, p)
+        }
+        // Make sure colors will be legible
+        val isDark = !ContrastColorUtil.isColorLight(bgColor)
+        fgColor = ContrastColorUtil.resolveContrastColor(context, fgColor, bgColor,
+                isDark)
+        fgColor = ContrastColorUtil.ensureTextContrast(fgColor, bgColor, isDark)
+
+        // Album art
+        var artwork: RoundedBitmapDrawable? = null
+        if (artworkBitmap != null) {
+            val original = artworkBitmap.copy(Bitmap.Config.ARGB_8888, true)
+            val scaled = Bitmap.createScaledBitmap(original, albumArtSize, albumArtSize,
+                    false)
+            artwork = RoundedBitmapDrawableFactory.create(context.resources, scaled)
+            artwork.cornerRadius = albumArtRadius.toFloat()
+        }
+
+        // App name
+        val builder = Notification.Builder.recoverBuilder(context, notif)
+        val app = builder.loadHeaderAppName()
+
+        // App Icon
+        val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context)
+
+        // Song name
+        val song: String = metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
+
+        // Artist name
+        val artist: String = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
+
+        // Control buttons
+        val actionIcons: MutableList<MediaAction> = ArrayList()
+        val actions = notif.actions
+        val actionsToShowCollapsed = notif.extras.getIntArray(
+                Notification.EXTRA_COMPACT_ACTIONS)?.toList() ?: emptyList()
+        // TODO: b/153736623 look into creating actions when this isn't a media style notification
+
+        val packageContext: Context = sbn.getPackageContext(context)
+        for (action in actions) {
+            val mediaAction = MediaAction(
+                    action.getIcon().loadDrawable(packageContext),
+                    action.actionIntent,
+                    action.title)
+            actionIcons.add(mediaAction)
+        }
+
+        foregroundExcecutor.execute {
+            onMediaDataLoaded(key, MediaData(true, fgColor, bgColor, app, smallIconDrawable, artist,
+                    song, artwork, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
+                    notif.contentIntent))
+        }
+
+    }
+
+    fun onMediaDataLoaded(key: String, data: MediaData) {
+        if (mediaEntries.containsKey(key)) {
+            // Otherwise this was removed already
+            mediaEntries.put(key, data)
+            listener.onMediaDataLoaded(key, data)
+        }
+    }
+
+    fun onNotificationRemoved(key: String) {
+        val removed = mediaEntries.remove(key)
+        if (removed != null) {
+            listener.onMediaDataRemoved(key)
+        }
+    }
+
+    private fun isMediaNotification(sbn: StatusBarNotification) : Boolean {
+        if (!useUniversalMediaPlayer()) {
+            return false
+        }
+        if (!sbn.notification.hasMediaSession()) {
+            return false
+        }
+        val notificationStyle = sbn.notification.notificationStyle
+        if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle)
+                || Notification.MediaStyle::class.java.equals(notificationStyle)) {
+            return true
+        }
+        return false
+    }
+
+    /**
+     * are we using the universal media player
+     */
+    private fun useUniversalMediaPlayer()
+            = Settings.System.getInt(context.contentResolver, "qs_media_player", 1) > 0
+
+    /**
+     * Are there any media notifications active?
+     */
+    fun hasActiveMedia() = mediaEntries.size > 0
+}
+
+private val LOADING = MediaData(false, 0, 0, null, null, null, null, null,
+        emptyList(), emptyList(), null, null, null)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
new file mode 100644
index 0000000..4a650ab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -0,0 +1,216 @@
+/*
+ * 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 android.annotation.IntDef
+import android.content.Context
+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
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.VisualStabilityManager
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.util.animation.UniqueObjectHost
+import com.android.systemui.util.concurrency.DelayableExecutor
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * This manager is responsible for placement of the unique media view between the different hosts
+ * and animate the positions of the views to achieve seamless transitions.
+ */
+@Singleton
+class MediaHierarchyManager @Inject constructor(
+    private val context: Context,
+    @Main private val foregroundExecutor: Executor,
+    @Background private val backgroundExecutor: DelayableExecutor,
+    private val localBluetoothManager: LocalBluetoothManager?,
+    private val visualStabilityManager: VisualStabilityManager,
+    private val statusBarStateController: StatusBarStateController,
+    private val bypassController: KeyguardBypassController,
+    mediaManager: NotificationMediaManager
+) {
+    private val mediaCarousel: ViewGroup
+    private val mediaContent: ViewGroup
+    private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
+    private val mediaHosts = arrayOfNulls<ViewGroup>(LOCATION_LOCKSCREEN + 1)
+    private val visualStabilityCallback = ::reorderAllPlayers
+    private var currentAttachmentLocation = -1
+
+    var shouldListen = true
+        set(value) {
+            field = value
+            for (player in mediaPlayers.values) {
+                player.setListening(shouldListen)
+            }
+        }
+
+    init {
+        mediaCarousel = inflateMediaCarousel()
+        mediaContent = mediaCarousel.findViewById(R.id.media_carousel)
+        mediaManager.addCallback(object : NotificationMediaManager.MediaListener {
+            override fun onMediaDataLoaded(key: String, data: MediaData) {
+                updateView(key, data)
+            }
+
+            override fun onMediaDataRemoved(key: String) {
+                val removed = mediaPlayers.remove(key)
+                removed?.apply {
+                    mediaContent.removeView(removed.view)
+                }
+            }
+        })
+    }
+
+    private fun inflateMediaCarousel(): ViewGroup {
+        return LayoutInflater.from(context).inflate(
+                R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup
+    }
+
+    private fun reorderAllPlayers() {
+        for (mediaPlayer in mediaPlayers.values) {
+            val view = mediaPlayer.view
+            if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) {
+                mediaContent.removeView(view)
+                mediaContent.addView(view, 0)
+            }
+        }
+    }
+
+    private fun updateView(key: String, data: MediaData) {
+        var existingPlayer = mediaPlayers[key]
+        if (existingPlayer == null) {
+            // Set up listener for device changes
+            // TODO: integrate with MediaTransferManager?
+            val imm = InfoMediaManager(context, data.packageName,
+                    null /* notification */, localBluetoothManager)
+            val routeManager = LocalMediaManager(context, localBluetoothManager,
+                    imm, data.packageName)
+
+            existingPlayer = MediaControlPanel(context, mediaContent, routeManager,
+                    foregroundExecutor, backgroundExecutor)
+            mediaPlayers[key] = existingPlayer
+            val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
+            val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT)
+            lp.marginStart = padding
+            lp.marginEnd = padding
+            existingPlayer.view.setLayoutParams(lp)
+            existingPlayer.setListening(shouldListen)
+            if (existingPlayer.isPlaying) {
+                mediaContent.addView(existingPlayer.view, 0)
+            } else {
+                mediaContent.addView(existingPlayer.view)
+            }
+        } else if (existingPlayer.isPlaying &&
+                mediaContent.indexOfChild(existingPlayer.view) != 0) {
+            if (visualStabilityManager.isReorderingAllowed) {
+                mediaContent.removeView(existingPlayer.view)
+                mediaContent.addView(existingPlayer.view, 0)
+            } else {
+                visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback)
+            }
+        }
+        existingPlayer.bind(data)
+    }
+
+    /**
+     * Create a media host view which can be attached to a view hierarchy and where the player
+     * will be placed in when the host is the currently desired state.
+     *
+     * @return the hostView associated with this location
+     */
+    fun createMediaHost(@MediaLocation location: Int) : ViewGroup {
+        val viewHost = UniqueObjectHost(context)
+        mediaHosts[location] = viewHost
+        if (location == currentAttachmentLocation) {
+            // 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
+            currentAttachmentLocation = -1
+        }
+        updateAttachmentLocation(animate = false)
+        return viewHost
+    }
+
+    private fun updateAttachmentLocation(animate: Boolean) {
+        var desiredLocation = calculateLocation()
+        if (desiredLocation != currentAttachmentLocation) {
+            val host = mediaHosts[desiredLocation]
+            host?.apply {
+                if (currentAttachmentLocation >= 0) {
+                    val currentHost = mediaHosts[currentAttachmentLocation]
+                    currentHost?.removeView(mediaCarousel)
+                }
+                host.addView(mediaCarousel)
+                // TODO: handle animation / repositioning etc
+                currentAttachmentLocation = desiredLocation
+            }
+        }
+    }
+
+    @MediaLocation
+    private fun calculateLocation() : Int {
+        val onLockscreen = (!bypassController.bypassEnabled
+                && (statusBarStateController.state == StatusBarState.KEYGUARD
+                || statusBarStateController.state == StatusBarState.FULLSCREEN_USER_SWITCHER))
+        return when {
+            qsExpansion > 0.0f -> LOCATION_QS
+            onLockscreen -> LOCATION_LOCKSCREEN
+            else -> LOCATION_QQS
+        }
+    }
+
+    /**
+     * The expansion of quick settings
+     */
+    var qsExpansion: Float = 0.0f
+        set(value) {
+            field = value
+            updateAttachmentLocation(animate = false)
+        }
+
+    @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class MediaLocation
+
+    companion object {
+        /**
+         * Attached in expanded quick settings
+         */
+        const val LOCATION_QS = 0
+
+        /**
+         * Attached in the collapsed QS
+         */
+        const val LOCATION_QQS = 1
+
+        /**
+         * Attached on the lock screen
+         */
+        const val LOCATION_LOCKSCREEN = 2
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index a0ea7fa..ce00229 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -269,14 +269,6 @@
             count++;
         }
 
-
-        if (Utils.useQsMediaPlayer(mQsPanel.getContext())) {
-            View qsMediaView = mQsPanel.getMediaPanel();
-            View qqsMediaView = mQuickQsPanel.getMediaPlayer().getView();
-            translationXBuilder.addFloat(qsMediaView, "alpha", 0, 1);
-            translationXBuilder.addFloat(qqsMediaView, "alpha", 1, 0);
-        }
-
         if (mAllowFancy) {
             // Make brightness appear static position and alpha in through second half.
             View brightness = mQsPanel.getBrightnessView();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
deleted file mode 100644
index 0571c68..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * Copyright (C) 2019 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.qs;
-
-import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.ColorStateList;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.media.MediaDescription;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.SeekBar;
-import android.widget.TextView;
-
-import androidx.constraintlayout.widget.ConstraintSet;
-import com.android.settingslib.media.LocalMediaManager;
-import com.android.internal.widget.MediaNotificationView;
-import com.android.systemui.R;
-import com.android.systemui.media.IlluminationDrawable;
-import com.android.systemui.media.MediaControlPanel;
-import com.android.systemui.media.SeekBarObserver;
-import com.android.systemui.media.SeekBarViewModel;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-
-import java.util.concurrent.Executor;
-
-/**
- * Single media player for carousel in QSPanel
- */
-public class QSMediaPlayer extends MediaControlPanel {
-
-    private static final String TAG = "QSMediaPlayer";
-
-    // Button IDs for QS controls
-    static final int[] QS_ACTION_IDS = {
-            R.id.action0,
-            R.id.action1,
-            R.id.action2,
-            R.id.action3,
-            R.id.action4
-    };
-
-    private final QSPanel mParent;
-    private final Executor mForegroundExecutor;
-    private final DelayableExecutor mBackgroundExecutor;
-    private final SeekBarViewModel mSeekBarViewModel;
-    private final SeekBarObserver mSeekBarObserver;
-    private String mPackageName;
-
-    /**
-     * Initialize quick shade version of player
-     * @param context
-     * @param parent
-     * @param routeManager Provides information about device
-     * @param foregroundExecutor
-     * @param backgroundExecutor
-     * @param activityStarter
-     */
-    public QSMediaPlayer(Context context, ViewGroup parent, LocalMediaManager routeManager,
-            Executor foregroundExecutor, DelayableExecutor backgroundExecutor,
-            ActivityStarter activityStarter) {
-        super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS,
-                foregroundExecutor, backgroundExecutor, activityStarter);
-        mParent = (QSPanel) parent;
-        mForegroundExecutor = foregroundExecutor;
-        mBackgroundExecutor = backgroundExecutor;
-        mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
-        mSeekBarObserver = new SeekBarObserver(getView());
-        // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
-        // priority of players. As soon as it is removed, the lifecycle will end and the seek bar
-        // will stop updating. So, use the lifecycle of the parent instead.
-        mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
-        SeekBar bar = getView().findViewById(R.id.media_progress_bar);
-        bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
-        bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
-    }
-
-    /**
-     * Add a media panel view based on a media description. Used for resumption
-     * @param description
-     * @param iconColor
-     * @param bgColor
-     * @param contentIntent
-     * @param pkgName
-     */
-    public void setMediaSession(MediaSession.Token token, MediaDescription description,
-            int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) {
-        mPackageName = pkgName;
-        PackageManager pm = getContext().getPackageManager();
-        Drawable icon = null;
-        CharSequence appName = pkgName.substring(pkgName.lastIndexOf("."));
-        try {
-            icon = pm.getApplicationIcon(pkgName);
-            appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0));
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.e(TAG, "Error getting package information", e);
-        }
-
-        // Set what we can normally
-        super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent,
-                appName.toString(), null);
-
-        // Then add info from MediaDescription
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
-        if (albumView != null) {
-            // Resize art in a background thread
-            mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView));
-        }
-
-        // Song name
-        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
-        CharSequence songName = description.getTitle();
-        titleText.setText(songName);
-        titleText.setTextColor(iconColor);
-
-        // Artist name (not in mini player)
-        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
-        if (artistText != null) {
-            CharSequence artistName = description.getSubtitle();
-            artistText.setText(artistName);
-            artistText.setTextColor(iconColor);
-        }
-
-        initLongPressMenu(iconColor);
-
-        // Set buttons to resume state
-        resetButtons();
-    }
-
-    /**
-     * Update media panel view for the given media session
-     * @param token token for this media session
-     * @param icon app notification icon
-     * @param largeIcon notification's largeIcon, used as a fallback for album art
-     * @param iconColor foreground color (for text, icons)
-     * @param bgColor background color
-     * @param actionsContainer a LinearLayout containing the media action buttons
-     * @param contentIntent Intent to send when user taps on player
-     * @param appName Application title
-     * @param key original notification's key
-     */
-    public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon,
-            int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent,
-            String appName, String key) {
-
-        super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName,
-                key);
-        ConstraintSet constraintSet = mMediaNotifView.getConstraintSet(R.id.expanded);
-        // Media controls
-        if (actionsContainer != null) {
-            LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-            int i = 0;
-            for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
-                final ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
-                ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
-                if (thatBtn == null || thatBtn.getDrawable() == null
-                        || thatBtn.getVisibility() != View.VISIBLE) {
-                    constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.GONE);
-                    continue;
-                }
-
-                if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) {
-                    ((IlluminationDrawable) mMediaNotifView.getBackground())
-                            .setupTouch(thisBtn, mMediaNotifView);
-                }
-
-                Drawable thatIcon = thatBtn.getDrawable();
-                thisBtn.setImageDrawable(thatIcon.mutate());
-                constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.VISIBLE);
-                thisBtn.setOnClickListener(v -> {
-                    Log.d(TAG, "clicking on other button");
-                    thatBtn.performClick();
-                });
-            }
-
-            // Hide any unused buttons
-            for (; i < QS_ACTION_IDS.length; i++) {
-                constraintSet.setVisibility(QS_ACTION_IDS[i], ConstraintSet.GONE);
-            }
-        }
-
-        // Seek Bar
-        final MediaController controller = new MediaController(getContext(), token);
-        mBackgroundExecutor.execute(
-                () -> mSeekBarViewModel.updateController(controller, iconColor));
-
-        initLongPressMenu(iconColor);
-    }
-
-    private void initLongPressMenu(int iconColor) {
-        // Set up long press menu
-        // TODO: b/156036025 bring back media guts
-    }
-
-    @Override
-    protected void resetButtons() {
-        super.resetButtons();
-        mSeekBarViewModel.clearController();
-//        View guts = mMediaNotifView.findViewById(R.id.media_guts);
-        View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
-
-        mMediaNotifView.setOnLongClickListener(v -> {
-            // Replace player view with close/cancel view
-            // TODO: b/156036025 make this work
-            options.setVisibility(View.VISIBLE);
-            return true; // consumed click
-        });
-    }
-
-    /**
-     * Sets the listening state of the player.
-     *
-     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
-     * unnecessary work when the QS panel is closed.
-     *
-     * @param listening True when player should be active. Otherwise, false.
-     */
-    public void setListening(boolean listening) {
-        mSeekBarViewModel.setListening(listening);
-    }
-
-    @Override
-    public void removePlayer() {
-        Log.d(TAG, "removing player from parent: " + mParent);
-        // Ensure this happens on the main thread (could happen in QSMediaBrowser callback)
-        mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this));
-    }
-
-    @Override
-    public String getMediaPlayerPackage() {
-        if (getController() == null) {
-            return mPackageName;
-        }
-        return super.getMediaPlayerPackage();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index e8f6c96..128eee8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -32,23 +32,19 @@
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
 import android.media.MediaDescription;
-import android.media.session.MediaSession;
 import android.metrics.LogMaker;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.service.notification.StatusBarNotification;
 import android.service.quicksettings.Tile;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.HorizontalScrollView;
 import android.widget.LinearLayout;
 
 import com.android.internal.logging.MetricsLogger;
@@ -56,17 +52,13 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.settingslib.Utils;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-import com.android.settingslib.media.InfoMediaManager;
-import com.android.settingslib.media.LocalMediaManager;
 import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.MediaControlPanel;
+import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSTile;
@@ -77,20 +69,17 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.BrightnessController;
 import com.android.systemui.settings.ToggleSliderView;
-import com.android.systemui.statusbar.notification.NotificationEntryListener;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
-import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -108,21 +97,14 @@
     protected final Context mContext;
     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
     private final BroadcastDispatcher mBroadcastDispatcher;
+    protected final MediaHierarchyManager mMediaHiearchyManager;
     private String mCachedSpecs = "";
     protected final View mBrightnessView;
     private final H mHandler = new H();
     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
     private final QSTileRevealController mQsTileRevealController;
 
-    private final LinearLayout mMediaCarousel;
-    private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
-    private final LocalBluetoothManager mLocalBluetoothManager;
-    private final Executor mForegroundExecutor;
-    private final DelayableExecutor mBackgroundExecutor;
-    private boolean mUpdateCarousel = false;
     private ActivityStarter mActivityStarter;
-    private NotificationEntryManager mNotificationEntryManager;
-
     protected boolean mExpanded;
     protected boolean mListening;
 
@@ -158,15 +140,6 @@
         }
     };
 
-    private final NotificationEntryListener mNotificationEntryListener =
-            new NotificationEntryListener() {
-        @Override
-        public void onEntryRemoved(NotificationEntry entry, NotificationVisibility visibility,
-                boolean removedByUser, int reason) {
-            checkToRemoveMediaNotification(entry);
-        }
-    };
-
     @Inject
     public QSPanel(
             @Named(VIEW_CONTEXT) Context context,
@@ -174,23 +147,17 @@
             DumpManager dumpManager,
             BroadcastDispatcher broadcastDispatcher,
             QSLogger qsLogger,
-            @Main Executor foregroundExecutor,
-            @Background DelayableExecutor backgroundExecutor,
-            @Nullable LocalBluetoothManager localBluetoothManager,
+            MediaHierarchyManager mediaHierarchyManager,
             ActivityStarter activityStarter,
-            NotificationEntryManager entryManager,
             UiEventLogger uiEventLogger
     ) {
         super(context, attrs);
+        mMediaHiearchyManager = mediaHierarchyManager;
         mContext = context;
         mQSLogger = qsLogger;
         mDumpManager = dumpManager;
-        mForegroundExecutor = foregroundExecutor;
-        mBackgroundExecutor = backgroundExecutor;
-        mLocalBluetoothManager = localBluetoothManager;
         mBroadcastDispatcher = broadcastDispatcher;
         mActivityStarter = activityStarter;
-        mNotificationEntryManager = entryManager;
         mUiEventLogger = uiEventLogger;
 
         setOrientation(VERTICAL);
@@ -210,158 +177,27 @@
 
         addDivider();
 
-        // Add media carousel
-        if (useQsMediaPlayer(context)) {
-            HorizontalScrollView mediaScrollView = (HorizontalScrollView) LayoutInflater.from(
-                    mContext).inflate(R.layout.media_carousel, this, false);
-            mMediaCarousel = mediaScrollView.findViewById(R.id.media_carousel);
-            addView(mediaScrollView, 0);
-        } else {
-            mMediaCarousel = null;
-        }
-
         mFooter = new QSSecurityFooter(this, context);
         addView(mFooter.getView());
 
+        // Add media carousel
+        if (useQsMediaPlayer(context)) {
+            addMediaHostView();
+        }
+
         updateResources();
 
         mBrightnessController = new BrightnessController(getContext(),
                 findViewById(R.id.brightness_slider), mBroadcastDispatcher);
     }
 
-    @Override
-    public void onVisibilityAggregated(boolean isVisible) {
-        super.onVisibilityAggregated(isVisible);
-        if (!isVisible && mUpdateCarousel) {
-            for (QSMediaPlayer player : mMediaPlayers) {
-                if (player.isPlaying()) {
-                    LayoutParams lp = (LayoutParams) player.getView().getLayoutParams();
-                    mMediaCarousel.removeView(player.getView());
-                    mMediaCarousel.addView(player.getView(), 0, lp);
-                    ((HorizontalScrollView) mMediaCarousel.getParent()).fullScroll(View.FOCUS_LEFT);
-                    mUpdateCarousel = false;
-                    break;
-                }
-            }
-        }
-    }
-
-    /**
-     * Add or update a player for the associated media session
-     * @param token
-     * @param icon
-     * @param largeIcon
-     * @param iconColor
-     * @param bgColor
-     * @param actionsContainer
-     * @param notif
-     * @param key
-     */
-    public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon,
-            int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif,
-            String key) {
-        if (!useQsMediaPlayer(mContext)) {
-            // Shouldn't happen, but just in case
-            Log.e(TAG, "Tried to add media session without player!");
-            return;
-        }
-        if (token == null) {
-            Log.e(TAG, "Media session token was null!");
-            return;
-        }
-
-        String packageName = notif.getPackageName();
-        QSMediaPlayer player = findMediaPlayer(packageName, token, key);
-
-        int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width);
-        int padding = (int) getResources().getDimension(R.dimen.qs_media_padding);
-        LayoutParams lp = new LayoutParams(playerWidth, ViewGroup.LayoutParams.MATCH_PARENT);
-        lp.setMarginStart(padding);
-        lp.setMarginEnd(padding);
-
-        if (player == null) {
-            Log.d(TAG, "creating new player for " + packageName);
-            // Set up listener for device changes
-            // TODO: integrate with MediaTransferManager?
-            InfoMediaManager imm = new InfoMediaManager(mContext, notif.getPackageName(),
-                    notif.getNotification(), mLocalBluetoothManager);
-            LocalMediaManager routeManager = new LocalMediaManager(mContext, mLocalBluetoothManager,
-                    imm, notif.getPackageName());
-
-            player = new QSMediaPlayer(mContext, this, routeManager, mForegroundExecutor,
-                    mBackgroundExecutor, mActivityStarter);
-            player.setListening(mListening);
-            if (player.isPlaying()) {
-                mMediaCarousel.addView(player.getView(), 0, lp); // add in front
-            } else {
-                mMediaCarousel.addView(player.getView(), lp); // add at end
-            }
-            mMediaPlayers.add(player);
-        } else if (player.isPlaying()) {
-            mUpdateCarousel = true;
-        }
-
-        Log.d(TAG, "setting player session");
-        String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification())
-                .loadHeaderAppName();
-        player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer,
-                notif.getNotification().contentIntent, appName, key);
-
-        if (mMediaPlayers.size() > 0) {
-            ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
-        }
-    }
-
-    /**
-     * Check for an existing media player using the given information
-     * @param packageName
-     * @param token
-     * @param key
-     * @return a player, or null if no match found
-     */
-    private QSMediaPlayer findMediaPlayer(String packageName, MediaSession.Token token,
-            String key) {
-        for (QSMediaPlayer player : mMediaPlayers) {
-            if (player.getKey() == null || key == null) {
-                // No notification key = loaded via mediabrowser, so just match on package
-                if (packageName.equals(player.getMediaPlayerPackage())) {
-                    Log.d(TAG, "Found matching resume player by package: " + packageName);
-                    return player;
-                }
-            } else if (player.getMediaSessionToken().equals(token)) {
-                Log.d(TAG, "Found matching player by token " + packageName);
-                return player;
-            } else if (packageName.equals(player.getMediaPlayerPackage())
-                    && key.equals(player.getKey())) {
-                // Also match if it's the same package and notification key
-                Log.d(TAG, "Found matching player by package " + packageName + ", " + key);
-                return player;
-            }
-        }
-        return null;
-    }
-
-    protected View getMediaPanel() {
-        return mMediaCarousel;
-    }
-
-    /**
-     * Remove the media player from the carousel
-     * @param player Player to remove
-     * @return true if removed, false if player was not found
-     */
-    protected boolean removeMediaPlayer(QSMediaPlayer player) {
-        // Remove from list
-        if (!mMediaPlayers.remove(player)) {
-            return false;
-        }
-
-        // Check if we need to collapse the carousel now
-        mMediaCarousel.removeView(player.getView());
-        if (mMediaPlayers.size() == 0) {
-            ((View) mMediaCarousel.getParent()).setVisibility(View.GONE);
-        }
-        return true;
+    protected void addMediaHostView() {
+        ViewGroup hostView = mMediaHiearchyManager.createMediaHost(
+                MediaHierarchyManager.LOCATION_QS);
+        addView(hostView);
+        ViewGroup.LayoutParams layoutParams = hostView.getLayoutParams();
+        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
     }
 
     private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() {
@@ -441,27 +277,6 @@
         mHasLoadedMediaControls = true;
     }
 
-    private void checkToRemoveMediaNotification(NotificationEntry entry) {
-        if (!useQsMediaPlayer(mContext)) {
-            return;
-        }
-
-        if (!entry.isMediaNotification()) {
-            return;
-        }
-
-        // If this entry corresponds to an existing set of controls, clear the controls
-        // This will handle apps that use an action to clear their notification
-        for (QSMediaPlayer p : mMediaPlayers) {
-            if (p.getKey() != null && p.getKey().equals(entry.getKey())) {
-                Log.d(TAG, "Clearing controls since notification removed " + entry.getKey());
-                p.clearControls();
-                return;
-            }
-        }
-        Log.d(TAG, "Media notification removed but no player found " + entry.getKey());
-    }
-
     protected void addDivider() {
         mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
         mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
@@ -528,7 +343,6 @@
                 loadMediaResumptionControls();
             }
         }
-        mNotificationEntryManager.addNotificationEntryListener(mNotificationEntryListener);
     }
 
     @Override
@@ -545,7 +359,6 @@
         }
         mDumpManager.unregisterDumpable(getDumpableTag());
         mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
-        mNotificationEntryManager.removeNotificationEntryListener(mNotificationEntryListener);
         super.onDetachedFromWindow();
     }
 
@@ -716,7 +529,7 @@
 
     public void setListening(boolean listening) {
         if (mListening == listening) return;
-        mListening = listening;
+        mMediaHiearchyManager.setShouldListen(listening);
         if (mTileLayout != null) {
             mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs);
             mTileLayout.setListening(listening);
@@ -724,9 +537,6 @@
         if (mListening) {
             refreshAllTiles();
         }
-        for (QSMediaPlayer player : mMediaPlayers) {
-            player.setListening(mListening);
-        }
     }
 
     private String getTilesSpecs() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
deleted file mode 100644
index 7b53eed..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2019 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.qs;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-import com.android.systemui.R;
-import com.android.systemui.media.IlluminationDrawable;
-import com.android.systemui.media.MediaControlPanel;
-import com.android.systemui.plugins.ActivityStarter;
-
-import java.util.concurrent.Executor;
-
-/**
- * QQS mini media player
- */
-public class QuickQSMediaPlayer extends MediaControlPanel {
-
-    private static final String TAG = "QQSMediaPlayer";
-
-    // Button IDs for QS controls
-    private static final int[] QQS_ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2};
-
-    /**
-     * Initialize mini media player for QQS
-     * @param context
-     * @param parent
-     * @param foregroundExecutor
-     * @param backgroundExecutor
-     * @param activityStarter
-     */
-    public QuickQSMediaPlayer(Context context, ViewGroup parent, Executor foregroundExecutor,
-            Executor backgroundExecutor, ActivityStarter activityStarter) {
-        super(context, parent, null, R.layout.qs_media_panel, QQS_ACTION_IDS,
-                foregroundExecutor, backgroundExecutor, activityStarter);
-    }
-
-    /**
-     * Update media panel view for the given media session
-     * @param token token for this media session
-     * @param icon app notification icon
-     * @param largeIcon notification's largeIcon, used as a fallback for album art
-     * @param iconColor foreground color (for text, icons)
-     * @param bgColor background color
-     * @param actionsContainer a LinearLayout containing the media action buttons
-     * @param actionsToShow indices of which actions to display in the mini player
-     *                      (max 3: Notification.MediaStyle.MAX_MEDIA_BUTTONS_IN_COMPACT)
-     * @param contentIntent Intent to send when user taps on the view
-     * @param key original notification's key
-     */
-    public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon,
-            int iconColor, int bgColor, View actionsContainer, int[] actionsToShow,
-            PendingIntent contentIntent, String key) {
-        // Only update if this is a different session and currently playing
-        String oldPackage = "";
-        if (getController() != null) {
-            oldPackage = getController().getPackageName();
-        }
-        MediaController controller = new MediaController(getContext(), token);
-        MediaSession.Token currentToken = getMediaSessionToken();
-        boolean samePlayer = currentToken != null
-                && currentToken.equals(token)
-                && oldPackage.equals(controller.getPackageName());
-        if (getController() != null && !samePlayer && !isPlaying(controller)) {
-            return;
-        }
-
-        super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key);
-
-        LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        int i = 0;
-        if (actionsToShow != null) {
-            int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount());
-            maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length);
-            for (; i < maxButtons; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]);
-                int thatId = NOTIF_ACTION_IDS[actionsToShow[i]];
-                ImageButton thatBtn = parentActionsLayout.findViewById(thatId);
-                if (thatBtn == null || thatBtn.getDrawable() == null
-                        || thatBtn.getVisibility() != View.VISIBLE) {
-                    thisBtn.setVisibility(View.GONE);
-                    continue;
-                }
-
-                if (mMediaNotifView.getBackground() instanceof IlluminationDrawable) {
-                    ((IlluminationDrawable) mMediaNotifView.getBackground())
-                            .setupTouch(thisBtn, mMediaNotifView);
-                }
-
-                Drawable thatIcon = thatBtn.getDrawable();
-                thisBtn.setImageDrawable(thatIcon.mutate());
-                thisBtn.setVisibility(View.VISIBLE);
-                thisBtn.setOnClickListener(v -> {
-                    thatBtn.performClick();
-                });
-            }
-        }
-
-        // Hide any unused buttons
-        for (; i < QQS_ACTION_IDS.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]);
-            thisBtn.setVisibility(View.GONE);
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 6683a1c..8dfa22f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -18,38 +18,36 @@
 
 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import androidx.annotation.NonNull;
 import com.android.internal.logging.UiEventLogger;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.media.MediaData;
+import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTile.SignalState;
 import com.android.systemui.plugins.qs.QSTile.State;
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.systemui.util.Utils;
-import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -63,20 +61,34 @@
     private static final String TAG = "QuickQSPanel";
     // Start it at 6 so a non-zero value can be obtained statically.
     private static int sDefaultMaxTiles = 6;
+    private final NotificationMediaManager mNotificationMediaManager;
 
     private boolean mDisabledByPolicy;
     private int mMaxTiles;
     protected QSPanel mFullPanel;
-    private QuickQSMediaPlayer mMediaPlayer;
     /** Whether or not the QS media player feature is enabled. */
     private boolean mUsingMediaPlayer;
     /** Whether or not the QuickQSPanel currently contains a media player. */
-    private boolean mHasMediaPlayer;
+    private boolean mShowHorizontalTileLayout;
     private LinearLayout mHorizontalLinearLayout;
 
     // Only used with media
-    private QSTileLayout mMediaTileLayout;
+    private QSTileLayout mHorizontalTileLayout;
     private QSTileLayout mRegularTileLayout;
+    private int mLastOrientation = -1;
+    private ViewGroup mMediaHost;
+    private final NotificationMediaManager.MediaListener mMediaCallback
+            = new NotificationMediaManager.MediaListener() {
+        @Override
+        public void onMediaDataLoaded(@NonNull String key, @NonNull MediaData data) {
+            switchTileLayout();
+        }
+
+        @Override
+        public void onMediaDataRemoved(@NonNull String key) {
+            switchTileLayout();
+        }
+    };
 
     @Inject
     public QuickQSPanel(
@@ -85,16 +97,14 @@
             DumpManager dumpManager,
             BroadcastDispatcher broadcastDispatcher,
             QSLogger qsLogger,
-            @Main Executor foregroundExecutor,
-            @Background DelayableExecutor backgroundExecutor,
-            @Nullable LocalBluetoothManager localBluetoothManager,
+            MediaHierarchyManager mediaHierarchyManager,
+            NotificationMediaManager notificationMediaManager,
             ActivityStarter activityStarter,
-            NotificationEntryManager entryManager,
             UiEventLogger uiEventLogger
     ) {
         super(context, attrs, dumpManager, broadcastDispatcher, qsLogger,
-                foregroundExecutor, backgroundExecutor, localBluetoothManager, activityStarter,
-                entryManager, uiEventLogger);
+                mediaHierarchyManager, activityStarter, uiEventLogger);
+        mNotificationMediaManager = notificationMediaManager;
         if (mFooter != null) {
             removeView(mFooter.getView());
         }
@@ -107,23 +117,17 @@
 
         mUsingMediaPlayer = Utils.useQsMediaPlayer(context);
         if (mUsingMediaPlayer) {
+            mNotificationMediaManager.addCallback(mMediaCallback);
             mHorizontalLinearLayout = new LinearLayout(mContext);
             mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
             mHorizontalLinearLayout.setClipChildren(false);
             mHorizontalLinearLayout.setClipToPadding(false);
 
-            int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing);
-            mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout,
-                    foregroundExecutor, backgroundExecutor, activityStarter);
-            LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
-            lp2.setMarginEnd(marginSize);
-            lp2.setMarginStart(0);
-            mHorizontalLinearLayout.addView(mMediaPlayer.getView(), lp2);
-
             mTileLayout = new DoubleLineTileLayout(context, mUiEventLogger);
-            mMediaTileLayout = mTileLayout;
+            mHorizontalTileLayout = mTileLayout;
             mRegularTileLayout = new HeaderTileLayout(context, mUiEventLogger);
             LayoutParams lp = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
+            int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing);
             lp.setMarginEnd(0);
             lp.setMarginStart(marginSize);
             mHorizontalLinearLayout.addView((View) mTileLayout, lp);
@@ -135,6 +139,7 @@
             ((View) mRegularTileLayout).setVisibility(View.GONE);
             addView((View) mRegularTileLayout, 0);
             super.setPadding(0, 0, 0, 0);
+            reAttachMediaHost();
         } else {
             sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns);
             mTileLayout = new HeaderTileLayout(context, mUiEventLogger);
@@ -144,8 +149,24 @@
         }
     }
 
-    public QuickQSMediaPlayer getMediaPlayer() {
-        return mMediaPlayer;
+    private void reAttachMediaHost() {
+        ViewGroup newParent = shouldUseHorizontalTileLayout() ? mHorizontalLinearLayout : this;
+        ViewGroup currentParent = (ViewGroup) mMediaHost.getParent();
+        if (currentParent != newParent) {
+            if (currentParent != null) {
+                currentParent.removeView(mMediaHost);
+            }
+            newParent.addView(mMediaHost);
+            ViewGroup.LayoutParams layoutParams = mMediaHost.getLayoutParams();
+            layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+            layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
+        }
+    }
+
+    @Override
+    protected void addMediaHostView() {
+        mMediaHost = mMediaHiearchyManager.createMediaHost(
+                MediaHierarchyManager.LOCATION_QQS);
     }
 
     @Override
@@ -167,6 +188,7 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         Dependency.get(TunerService.class).removeTunable(mNumTiles);
+        mNotificationMediaManager.removeCallback(mMediaCallback);
     }
 
     @Override
@@ -191,10 +213,19 @@
         super.drawTile(r, state);
     }
 
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (newConfig.orientation != mLastOrientation) {
+            mLastOrientation = newConfig.orientation;
+            switchTileLayout();
+        }
+    }
+
     boolean switchTileLayout() {
         if (!mUsingMediaPlayer) return false;
-        mHasMediaPlayer = mMediaPlayer.hasMediaSession();
-        if (mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.GONE) {
+        mShowHorizontalTileLayout = shouldUseHorizontalTileLayout();
+        if (mShowHorizontalTileLayout && mHorizontalLinearLayout.getVisibility() == View.GONE) {
             mHorizontalLinearLayout.setVisibility(View.VISIBLE);
             ((View) mRegularTileLayout).setVisibility(View.GONE);
             mTileLayout.setListening(false);
@@ -202,11 +233,13 @@
                 mTileLayout.removeTile(record);
                 record.tile.removeCallback(record.callback);
             }
-            mTileLayout = mMediaTileLayout;
+            mTileLayout = mHorizontalTileLayout;
             if (mHost != null) setTiles(mHost.getTiles());
             mTileLayout.setListening(mListening);
+            reAttachMediaHost();
             return true;
-        } else if (!mHasMediaPlayer && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) {
+        } else if (!mShowHorizontalTileLayout
+                && mHorizontalLinearLayout.getVisibility() == View.VISIBLE) {
             mHorizontalLinearLayout.setVisibility(View.GONE);
             ((View) mRegularTileLayout).setVisibility(View.VISIBLE);
             mTileLayout.setListening(false);
@@ -217,14 +250,21 @@
             mTileLayout = mRegularTileLayout;
             if (mHost != null) setTiles(mHost.getTiles());
             mTileLayout.setListening(mListening);
+            reAttachMediaHost();
             return true;
         }
         return false;
     }
 
-    /** Returns true if this panel currently contains a media player. */
-    public boolean hasMediaPlayer() {
-        return mHasMediaPlayer;
+    private boolean shouldUseHorizontalTileLayout() {
+        return mNotificationMediaManager.hasActiveMedia()
+                && getResources().getConfiguration().orientation
+                        == Configuration.ORIENTATION_LANDSCAPE;
+    }
+
+    /** Returns true if this panel currently uses a horizontal tile layout. */
+    public boolean usesHorizontalLayout() {
+        return mShowHorizontalTileLayout;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index b15c6a3..b14697d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -341,7 +341,7 @@
         if (mQsDisabled) {
             lp.height = resources.getDimensionPixelSize(
                     com.android.internal.R.dimen.quick_qs_offset_height);
-        } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.hasMediaPlayer()) {
+        } else if (useQsMediaPlayer(mContext) && mHeaderQsPanel.usesHorizontalLayout()) {
             lp.height = Math.max(getMinimumHeight(),
                     resources.getDimensionPixelSize(
                             com.android.internal.R.dimen.quick_qs_total_height_with_media));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 459c95b..63ba57ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -44,14 +44,17 @@
 import android.view.View;
 import android.widget.ImageView;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.statusbar.NotificationVisibility;
-import com.android.keyguard.KeyguardMediaPlayer;
 import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.Interpolators;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.media.MediaData;
+import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.dagger.StatusBarModule;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -103,6 +106,7 @@
     }
 
     private final NotificationEntryManager mEntryManager;
+    private final MediaDataManager mMediaDataManager;
 
     @Nullable
     private Lazy<NotificationShadeWindowController> mNotificationShadeWindowController;
@@ -113,7 +117,6 @@
     private ScrimController mScrimController;
     @Nullable
     private LockscreenWallpaper mLockscreenWallpaper;
-    private final KeyguardMediaPlayer mMediaPlayer;
 
     private final Executor mMainExecutor;
 
@@ -187,13 +190,12 @@
             NotificationEntryManager notificationEntryManager,
             MediaArtworkProcessor mediaArtworkProcessor,
             KeyguardBypassController keyguardBypassController,
-            KeyguardMediaPlayer keyguardMediaPlayer,
             @Main Executor mainExecutor,
-            DeviceConfigProxy deviceConfig) {
+            DeviceConfigProxy deviceConfig,
+            MediaDataManager mediaDataManager) {
         mContext = context;
         mMediaArtworkProcessor = mediaArtworkProcessor;
         mKeyguardBypassController = keyguardBypassController;
-        mMediaPlayer = keyguardMediaPlayer;
         mMediaListeners = new ArrayList<>();
         // TODO: use MediaSessionManager.SessionListener to hook us up to future updates
         // in session state
@@ -204,7 +206,37 @@
         mNotificationShadeWindowController = notificationShadeWindowController;
         mEntryManager = notificationEntryManager;
         mMainExecutor = mainExecutor;
+        mMediaDataManager = mediaDataManager;
+        mediaDataManager.setListener(new MediaListener() {
+            @Override
+            public void onMediaDataLoaded(@NonNull String key, @NonNull MediaData metadata) {
+                ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners);
+                for (int i = 0; i < callbacks.size(); i++) {
+                    callbacks.get(i).onMediaDataLoaded(key, metadata);
+                }
+            }
+
+            @Override
+            public void onMediaDataRemoved(@NonNull String key) {
+                ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners);
+                for (int i = 0; i < callbacks.size(); i++) {
+                    callbacks.get(i).onMediaDataRemoved(key);
+                }
+            }
+        });
+
         notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
+
+            @Override
+            public void onPendingEntryAdded(NotificationEntry entry) {
+                mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
+            }
+
+            @Override
+            public void onPreEntryUpdated(NotificationEntry entry) {
+                mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
+            }
+
             @Override
             public void onEntryInflated(NotificationEntry entry) {
                 findAndUpdateMediaNotifications();
@@ -222,6 +254,7 @@
                     boolean removedByUser,
                     int reason) {
                 onNotificationRemoved(entry.getKey());
+                mMediaDataManager.onNotificationRemoved(entry.getKey());
             }
         });
 
@@ -278,7 +311,7 @@
 
     public void addCallback(MediaListener callback) {
         mMediaListeners.add(callback);
-        callback.onMetadataOrStateChanged(mMediaMetadata,
+        callback.onPrimaryMetadataOrStateChanged(mMediaMetadata,
                 getMediaControllerPlaybackState(mMediaController));
     }
 
@@ -392,7 +425,7 @@
         @PlaybackState.State int state = getMediaControllerPlaybackState(mMediaController);
         ArrayList<MediaListener> callbacks = new ArrayList<>(mMediaListeners);
         for (int i = 0; i < callbacks.size(); i++) {
-            callbacks.get(i).onMetadataOrStateChanged(mMediaMetadata, state);
+            callbacks.get(i).onPrimaryMetadataOrStateChanged(mMediaMetadata, state);
         }
     }
 
@@ -473,7 +506,6 @@
             && mBiometricUnlockController.isWakeAndUnlock();
         if (mKeyguardStateController.isLaunchTransitionFadingAway() || wakeAndUnlock) {
             mBackdrop.setVisibility(View.INVISIBLE);
-            mMediaPlayer.clearControls();
             Trace.endSection();
             return;
         }
@@ -496,14 +528,6 @@
             }
         }
 
-        NotificationEntry entry = mEntryManager
-                .getActiveNotificationUnfiltered(mMediaNotificationKey);
-        if (entry != null) {
-            mMediaPlayer.updateControls(entry, getMediaIcon(), mediaMetadata);
-        } else {
-            mMediaPlayer.clearControls();
-        }
-
         // Process artwork on a background thread and send the resulting bitmap to
         // finishUpdateMediaMetaData.
         if (metaDataChanged) {
@@ -626,7 +650,6 @@
                     // We are unlocking directly - no animation!
                     mBackdrop.setVisibility(View.GONE);
                     mBackdropBack.setImageDrawable(null);
-                    mMediaPlayer.clearControls();
                     if (windowController != null) {
                         windowController.setBackdropShowing(false);
                     }
@@ -643,7 +666,6 @@
                                 mBackdrop.setVisibility(View.GONE);
                                 mBackdropFront.animate().cancel();
                                 mBackdropBack.setImageDrawable(null);
-                                mMediaPlayer.clearControls();
                                 mMainExecutor.execute(mHideBackdropFront);
                             });
                     if (mKeyguardStateController.isKeyguardFadingAway()) {
@@ -698,6 +720,13 @@
     }
 
     /**
+     * Are there any media notifications active?
+     */
+    public boolean hasActiveMedia() {
+        return mMediaDataManager.hasActiveMedia();
+    }
+
+    /**
      * {@link AsyncTask} to prepare album art for use as backdrop on lock screen.
      */
     private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> {
@@ -750,6 +779,17 @@
          * @param state Current playback state
          * @see PlaybackState.State
          */
-        void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state);
+        default void onPrimaryMetadataOrStateChanged(MediaMetadata metadata,
+                @PlaybackState.State int state) {}
+
+        /**
+         * Called whenever there's new MediaData Loaded for the consumption in views
+         */
+        default void onMediaDataLoaded(@NonNull String key, @NonNull MediaData data) {}
+
+        /**
+         * Called whenever a previously existing Media notification was removed
+         */
+        default void onMediaDataRemoved(@NonNull String key) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index de7e36d9..f0fed13 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -21,9 +21,9 @@
 import android.os.Handler;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.KeyguardMediaPlayer;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.MediaArtworkProcessor;
@@ -95,9 +95,9 @@
             NotificationEntryManager notificationEntryManager,
             MediaArtworkProcessor mediaArtworkProcessor,
             KeyguardBypassController keyguardBypassController,
-            KeyguardMediaPlayer keyguardMediaPlayer,
             @Main Executor mainExecutor,
-            DeviceConfigProxy deviceConfigProxy) {
+            DeviceConfigProxy deviceConfigProxy,
+            MediaDataManager mediaDataManager) {
         return new NotificationMediaManager(
                 context,
                 statusBarLazy,
@@ -105,9 +105,9 @@
                 notificationEntryManager,
                 mediaArtworkProcessor,
                 keyguardBypassController,
-                keyguardMediaPlayer,
                 mainExecutor,
-                deviceConfigProxy);
+                deviceConfigProxy,
+                mediaDataManager);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index cb0c283..634872d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -209,7 +209,7 @@
     }
 
     /** The key for this notification. Guaranteed to be immutable and unique */
-    public String getKey() {
+    @NonNull public String getKey() {
         return mKey;
     }
 
@@ -217,7 +217,7 @@
      * The StatusBarNotification that represents one half of a NotificationEntry (the other half
      * being the Ranking). This object is swapped out whenever a notification is updated.
      */
-    public StatusBarNotification getSbn() {
+    @NonNull public StatusBarNotification getSbn() {
         return mSbn;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
index 59f119e..3fab6f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionListener.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection.notifcollection;
 
+import android.annotation.NonNull;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 
@@ -43,13 +44,13 @@
      * there is no guarantee of order and they may not have had a chance to initialize yet. Instead,
      * use {@link #onEntryAdded} which is called after all initialization.
      */
-    default void onEntryInit(NotificationEntry entry) {
+    default void onEntryInit(@NonNull NotificationEntry entry) {
     }
 
     /**
      * Called whenever a notification with a new key is posted.
      */
-    default void onEntryAdded(NotificationEntry entry) {
+    default void onEntryAdded(@NonNull NotificationEntry entry) {
     }
 
     /**
@@ -64,7 +65,7 @@
      * immediately after a user dismisses a notification: we wait until we receive confirmation from
      * system server before considering the notification removed.
      */
-    default void onEntryRemoved(NotificationEntry entry, @CancellationReason int reason) {
+    default void onEntryRemoved(@NonNull NotificationEntry entry, @CancellationReason int reason) {
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt
index 88888d1..0fd865b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/BypassHeadsUpNotifier.kt
@@ -75,7 +75,7 @@
         mediaManager.addCallback(this)
     }
 
-    override fun onMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) {
+    override fun onPrimaryMetadataOrStateChanged(metadata: MediaMetadata?, state: Int) {
         val previous = currentMediaEntry
         var newEntry = entryManager
                 .getActiveNotificationUnfiltered(mediaManager.mediaNotificationKey)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
index b96cff8..93d3f3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
@@ -178,38 +178,6 @@
         final MediaSession.Token token = mRow.getEntry().getSbn().getNotification().extras
                 .getParcelable(Notification.EXTRA_MEDIA_SESSION);
 
-        if (Utils.useQsMediaPlayer(mContext) && token != null) {
-            final int[] compactActions = mRow.getEntry().getSbn().getNotification().extras
-                    .getIntArray(Notification.EXTRA_COMPACT_ACTIONS);
-            int tintColor = getNotificationHeader().getOriginalIconColor();
-            NotificationShadeWindowController ctrl = Dependency.get(
-                    NotificationShadeWindowController.class);
-            QuickQSPanel panel = ctrl.getNotificationShadeView().findViewById(
-                    com.android.systemui.R.id.quick_qs_panel);
-            StatusBarNotification sbn = mRow.getEntry().getSbn();
-            Notification notif = sbn.getNotification();
-            Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext);
-            panel.getMediaPlayer().setMediaSession(token,
-                    iconDrawable,
-                    notif.getLargeIcon(),
-                    tintColor,
-                    mBackgroundColor,
-                    mActions,
-                    compactActions,
-                    notif.contentIntent,
-                    sbn.getKey());
-            QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById(
-                    com.android.systemui.R.id.quick_settings_panel);
-            bigPanel.addMediaSession(token,
-                    iconDrawable,
-                    notif.getLargeIcon(),
-                    tintColor,
-                    mBackgroundColor,
-                    mActions,
-                    sbn,
-                    sbn.getKey());
-        }
-
         boolean showCompactSeekbar = mMediaManager.getShowCompactMediaSeekbar();
         if (token == null || (COMPACT_MEDIA_TAG.equals(mView.getTag()) && !showCompactSeekbar)) {
             if (mSeekBarView != null) {
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 ab055e1..82560f5 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
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup;
 
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -37,7 +38,6 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mContentView = findViewById(R.id.keyguard_media_view);
     }
 
     @Override
@@ -52,4 +52,12 @@
     public void setBackgroundColor(int color) {
         setTintColor(color);
     }
+
+    public void setMediaHost(ViewGroup mediaHost) {
+        mContentView = mediaHost;
+        addView(mediaHost);
+        ViewGroup.LayoutParams layoutParams = mediaHost.getLayoutParams();
+        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
index 6eec1ca..1d01db4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java
@@ -29,10 +29,12 @@
 import android.provider.Settings;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.keyguard.KeyguardMediaPlayer;
 import com.android.systemui.R;
+import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -74,8 +76,8 @@
     private final StatusBarStateController mStatusBarStateController;
     private final ConfigurationController mConfigurationController;
     private final PeopleHubViewAdapter mPeopleHubViewAdapter;
-    private final KeyguardMediaPlayer mKeyguardMediaPlayer;
     private final NotificationSectionsFeatureManager mSectionsFeatureManager;
+    private final KeyguardMediaController mKeyguardMediaController;
     private final int mNumberOfSections;
 
     private final PeopleHubViewBoundary mPeopleHubViewBoundary = new PeopleHubViewBoundary() {
@@ -123,15 +125,16 @@
             StatusBarStateController statusBarStateController,
             ConfigurationController configurationController,
             PeopleHubViewAdapter peopleHubViewAdapter,
-            KeyguardMediaPlayer keyguardMediaPlayer,
+            MediaHierarchyManager mediaHiearchyManager,
+            KeyguardMediaController keyguardMediaController,
             NotificationSectionsFeatureManager sectionsFeatureManager) {
         mActivityStarter = activityStarter;
         mStatusBarStateController = statusBarStateController;
         mConfigurationController = configurationController;
         mPeopleHubViewAdapter = peopleHubViewAdapter;
-        mKeyguardMediaPlayer = keyguardMediaPlayer;
         mSectionsFeatureManager = sectionsFeatureManager;
         mNumberOfSections = mSectionsFeatureManager.getNumberOfBuckets();
+        mKeyguardMediaController = keyguardMediaController;
     }
 
     NotificationSection[] createSectionsForBuckets() {
@@ -205,12 +208,9 @@
         mIncomingHeader.setHeaderText(R.string.notification_section_header_incoming);
         mIncomingHeader.setOnHeaderClickListener(this::onGentleHeaderClick);
 
-        if (mMediaControlsView != null) {
-            mKeyguardMediaPlayer.unbindView();
-        }
         mMediaControlsView = reinflateView(mMediaControlsView, layoutInflater,
                 R.layout.keyguard_media_header);
-        mKeyguardMediaPlayer.bindView(mMediaControlsView);
+        mKeyguardMediaController.attach(mMediaControlsView);
     }
 
     /** Listener for when the "clear all" button is clicked on the gentle notification header. */
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 c9716d3..aa6baff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -69,6 +69,7 @@
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
+import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -242,6 +243,7 @@
     private final KeyguardBypassController mKeyguardBypassController;
     private final KeyguardUpdateMonitor mUpdateMonitor;
     private final ConversationNotificationManager mConversationNotificationManager;
+    private final MediaHierarchyManager mMediaHierarchyManager;
 
     private KeyguardAffordanceHelper mAffordanceHelper;
     private KeyguardUserSwitcher mKeyguardUserSwitcher;
@@ -456,7 +458,8 @@
             ConfigurationController configurationController,
             FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
-            ConversationNotificationManager conversationNotificationManager) {
+            ConversationNotificationManager conversationNotificationManager,
+            MediaHierarchyManager mediaHierarchyManager) {
         super(view, falsingManager, dozeLog, keyguardStateController,
                 (SysuiStatusBarStateController) statusBarStateController, vibratorHelper,
                 latencyTracker, flingAnimationUtilsBuilder, statusBarTouchableRegionManager);
@@ -466,6 +469,7 @@
         mZenModeController = zenModeController;
         mConfigurationController = configurationController;
         mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
+        mMediaHierarchyManager = mediaHierarchyManager;
         mView.setWillNotDraw(!DEBUG);
         mInjectionInflationController = injectionInflationController;
         mFalsingManager = falsingManager;
@@ -1609,7 +1613,7 @@
         if (mQs == null) return;
         float qsExpansionFraction = getQsExpansionFraction();
         mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation());
-        int heightDiff = mQs.getDesiredHeight() - mQs.getQsMinExpansionHeight();
+        mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
         mNotificationStackScroller.setQsExpansionFraction(qsExpansionFraction);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt
new file mode 100644
index 0000000..63fc6c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHost.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.util.animation
+
+import android.content.Context
+import android.widget.FrameLayout
+
+/**
+ * A special view that hosts a unique object, which only exists once, but can transition between
+ * different hosts. If the view currently hosts the unique object, it's measuring it normally,
+ * but if it's not attached, it will obtain the size by requesting a measure.
+ */
+class UniqueObjectHost(context: Context) : FrameLayout(context) {
+    private var cachedHeight: Int = 0
+    private var cachedWidth: Int = 0
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        if (isCurrentHost()) {
+            cachedHeight = measuredHeight
+            cachedWidth = measuredWidth
+        } else {
+            setMeasuredDimension(cachedWidth, cachedHeight)
+        }
+    }
+
+    private fun isCurrentHost() = childCount != 0
+}