Reset when playback state is NONE, and refactoring

If a notification is cleared it won't always send an update first and
call onSessionDestroyed. NotificationMediaManager already checks if the
current media notification is being removed and sends an event, so we
can listen for that.

This fixes the bug with cast notifications. It can also be tested by
playing media, pausing, and then clearing all notifications - the
controls will now switch to the resumption state.

Also refactored to put common code for QQS and QS players in a single
class, and removed the long-press menu in QSMediaPlayer (which was just for
testing)

Fixes: 150437753
Fixes: 150742919
Bug: 150854549
Test: manual; atest com.android.systemui.qs com.android.systemui.statusbar.notification
Change-Id: I0ecada16967a6d21b82dc86d05544cf78027bcd9
diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml
index 22303dc..34bd703 100644
--- a/packages/SystemUI/res/layout/qs_media_panel.xml
+++ b/packages/SystemUI/res/layout/qs_media_panel.xml
@@ -32,7 +32,6 @@
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:id="@+id/header"
         android:layout_marginBottom="16dp"
     >
 
@@ -73,7 +72,7 @@
 
             <!-- Song name -->
             <TextView
-                android:id="@+id/header_text"
+                android:id="@+id/header_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:singleLine="true"
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
new file mode 100644
index 0000000..a161d03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -0,0 +1,425 @@
+/*
+ * 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.LayoutRes;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.RippleDrawable;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.media.MediaOutputSliceConstants;
+import com.android.settingslib.widget.AdaptiveIcon;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.NotificationMediaManager;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base media control panel for System UI
+ */
+public class MediaControlPanel implements NotificationMediaManager.MediaListener {
+    private static final String TAG = "MediaControlPanel";
+    private final NotificationMediaManager mMediaManager;
+    private final Executor mBackgroundExecutor;
+
+    private Context mContext;
+    protected LinearLayout mMediaNotifView;
+    private View mSeamless;
+    private MediaSession.Token mToken;
+    private MediaController mController;
+    private int mForegroundColor;
+    private int mBackgroundColor;
+    protected ComponentName mRecvComponent;
+
+    private final int[] mActionIds;
+
+    // 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
+    };
+
+    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
+        @Override
+        public void onSessionDestroyed() {
+            Log.d(TAG, "session destroyed");
+            mController.unregisterCallback(mSessionCallback);
+            clearControls();
+        }
+    };
+
+    /**
+     * Initialize a new control panel
+     * @param context
+     * @param parent
+     * @param manager
+     * @param layoutId layout resource to use for this control panel
+     * @param actionIds resource IDs for action buttons in the layout
+     * @param backgroundExecutor background executor, used for processing artwork
+     */
+    public MediaControlPanel(Context context, ViewGroup parent, NotificationMediaManager manager,
+            @LayoutRes int layoutId, int[] actionIds, Executor backgroundExecutor) {
+        mContext = context;
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
+        mMediaManager = manager;
+        mActionIds = actionIds;
+        mBackgroundExecutor = backgroundExecutor;
+    }
+
+    /**
+     * Get the view used to display media controls
+     * @return the view
+     */
+    public View getView() {
+        return mMediaNotifView;
+    }
+
+    /**
+     * Get the context
+     * @return context
+     */
+    public Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Update the media panel view for the given media session
+     * @param token
+     * @param icon
+     * @param iconColor
+     * @param bgColor
+     * @param contentIntent
+     * @param appNameString
+     * @param device
+     */
+    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
+            int bgColor, PendingIntent contentIntent, String appNameString, MediaDevice device) {
+        mToken = token;
+        mForegroundColor = iconColor;
+        mBackgroundColor = bgColor;
+        mController = new MediaController(mContext, mToken);
+
+        MediaMetadata mediaMetadata = mController.getMetadata();
+
+        // Try to find a receiver for the media button that matches this app
+        PackageManager pm = mContext.getPackageManager();
+        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
+        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
+        if (info != null) {
+            for (ResolveInfo inf : info) {
+                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
+                    mRecvComponent = inf.getComponentInfo().getComponentName();
+                }
+            }
+        }
+
+        mController.registerCallback(mSessionCallback);
+
+        if (mediaMetadata == null) {
+            Log.e(TAG, "Media metadata was null");
+            return;
+        }
+
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        if (albumView != null) {
+            // Resize art in a background thread
+            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
+        }
+        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
+
+        // Click action
+        mMediaNotifView.setOnClickListener(v -> {
+            try {
+                contentIntent.send();
+                // Also close shade
+                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+            } catch (PendingIntent.CanceledException e) {
+                Log.e(TAG, "Pending intent was canceled", e);
+            }
+        });
+
+        // App icon
+        ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
+        Drawable iconDrawable = icon.loadDrawable(mContext);
+        iconDrawable.setTint(mForegroundColor);
+        appIcon.setImageDrawable(iconDrawable);
+
+        // Song name
+        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+        titleText.setText(songName);
+        titleText.setTextColor(mForegroundColor);
+
+        // Not in mini player:
+        // App title
+        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
+        if (appName != null) {
+            appName.setText(appNameString);
+            appName.setTextColor(mForegroundColor);
+        }
+
+        // Artist name
+        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+        if (artistText != null) {
+            String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+            artistText.setText(artistName);
+            artistText.setTextColor(mForegroundColor);
+        }
+
+        // Transfer chip
+        mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
+        if (mSeamless != null) {
+            mSeamless.setVisibility(View.VISIBLE);
+            updateDevice(device);
+            ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
+            mSeamless.setOnClickListener(v -> {
+                final Intent intent = new Intent()
+                        .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
+                        .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
+                                mController.getPackageName())
+                        .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
+                mActivityStarter.startActivity(intent, false, true /* dismissShade */,
+                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            });
+        }
+
+        // Ensure is only added once
+        mMediaManager.removeCallback(this);
+        mMediaManager.addCallback(this);
+    }
+
+    /**
+     * Return the token for the current media session
+     * @return the token
+     */
+    public MediaSession.Token getMediaSessionToken() {
+        return mToken;
+    }
+
+    /**
+     * Get the current media controller
+     * @return the controller
+     */
+    public MediaController getController() {
+        return mController;
+    }
+
+    /**
+     * Get the name of the package associated with the current media controller
+     * @return the package name
+     */
+    public String getMediaPlayerPackage() {
+        return mController.getPackageName();
+    }
+
+    /**
+     * Check whether this player has an attached media session.
+     * @return whether there is a controller with a current media session.
+     */
+    public boolean hasMediaSession() {
+        return mController != null && mController.getPlaybackState() != null;
+    }
+
+    /**
+     * Check whether the media controlled by this player is currently playing
+     * @return whether it is playing, or false if no controller information
+     */
+    public boolean isPlaying() {
+        return isPlaying(mController);
+    }
+
+    /**
+     * Check whether the given controller is currently playing
+     * @param controller media controller to check
+     * @return whether it is playing, or false if no controller information
+     */
+    protected boolean isPlaying(MediaController controller) {
+        if (controller == null) {
+            return false;
+        }
+
+        PlaybackState state = controller.getPlaybackState();
+        if (state == null) {
+            return false;
+        }
+
+        return (state.getState() == PlaybackState.STATE_PLAYING);
+    }
+
+    /**
+     * Process album art for layout
+     * @param metadata media metadata
+     * @param albumView view to hold the album art
+     */
+    private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
+        Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+        float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
+        RoundedBitmapDrawable roundedDrawable = null;
+        if (albumArt != null) {
+            Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
+            int albumSize = (int) mContext.getResources().getDimension(
+                    R.dimen.qs_media_album_size);
+            Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
+            roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
+            roundedDrawable.setCornerRadius(radius);
+        } else {
+            Log.e(TAG, "No album art available");
+        }
+
+        // Now that it's resized, update the UI
+        final RoundedBitmapDrawable result = roundedDrawable;
+        albumView.getHandler().post(() -> {
+            if (result != null) {
+                albumView.setImageDrawable(result);
+                albumView.setVisibility(View.VISIBLE);
+            } else {
+                albumView.setImageDrawable(null);
+                albumView.setVisibility(View.GONE);
+            }
+        });
+    }
+
+    /**
+     * Update the current device information
+     * @param device device information to display
+     */
+    public void updateDevice(MediaDevice device) {
+        if (mSeamless == null) {
+            return;
+        }
+        Handler handler = mSeamless.getHandler();
+        handler.post(() -> {
+            updateChipInternal(device);
+        });
+    }
+
+    private void updateChipInternal(MediaDevice device) {
+        ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
+
+        // Update the outline color
+        LinearLayout viewLayout = (LinearLayout) mSeamless;
+        RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
+        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
+        rect.setStroke(2, mForegroundColor);
+        rect.setColor(mBackgroundColor);
+
+        ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
+        TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
+        deviceName.setTextColor(fgTintList);
+
+        if (device != null) {
+            Drawable icon = device.getIcon();
+            iconView.setVisibility(View.VISIBLE);
+            iconView.setImageTintList(fgTintList);
+
+            if (icon instanceof AdaptiveIcon) {
+                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
+                aIcon.setBackgroundColor(mBackgroundColor);
+                iconView.setImageDrawable(aIcon);
+            } else {
+                iconView.setImageDrawable(icon);
+            }
+            deviceName.setText(device.getName());
+        } else {
+            // Reset to default
+            iconView.setVisibility(View.GONE);
+            deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
+        }
+    }
+
+    /**
+     * Put controls into a resumption state
+     */
+    public void clearControls() {
+        // 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);
+            }
+        }
+
+        // Add a restart button
+        ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
+        btn.setOnClickListener(v -> {
+            Log.d(TAG, "Attempting to restart session");
+            // Send a media button event to previously found receiver
+            if (mRecvComponent != null) {
+                Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+                intent.setComponent(mRecvComponent);
+                int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
+                intent.putExtra(
+                        Intent.EXTRA_KEY_EVENT,
+                        new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+                mContext.sendBroadcast(intent);
+            } else {
+                Log.d(TAG, "No receiver to restart");
+                // If we don't have a receiver, try relaunching the activity instead
+                try {
+                    mController.getSessionActivity().send();
+                } catch (PendingIntent.CanceledException e) {
+                    Log.e(TAG, "Pending intent was canceled", e);
+                }
+            }
+        });
+        btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
+        btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
+        btn.setVisibility(View.VISIBLE);
+    }
+
+    @Override
+    public void onMetadataOrStateChanged(MediaMetadata metadata, int state) {
+        if (state == PlaybackState.STATE_NONE) {
+            clearControls();
+            mMediaManager.removeCallback(this);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
index 011893d..837256b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
@@ -17,158 +17,53 @@
 package com.android.systemui.qs;
 
 import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.Icon;
-import android.graphics.drawable.RippleDrawable;
-import android.media.MediaMetadata;
-import android.media.session.MediaController;
 import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Handler;
 import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
 
 import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.media.MediaOutputSliceConstants;
-import com.android.settingslib.widget.AdaptiveIcon;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.media.MediaControlPanel;
+import com.android.systemui.statusbar.NotificationMediaManager;
 
-import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Single media player for carousel in QSPanel
  */
-public class QSMediaPlayer {
+public class QSMediaPlayer extends MediaControlPanel {
 
     private static final String TAG = "QSMediaPlayer";
 
-    private Context mContext;
-    private LinearLayout mMediaNotifView;
-    private View mSeamless;
-    private MediaSession.Token mToken;
-    private MediaController mController;
-    private int mForegroundColor;
-    private int mBackgroundColor;
-    private ComponentName mRecvComponent;
-    private QSPanel mParent;
-
-    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
-        @Override
-        public void onSessionDestroyed() {
-            Log.d(TAG, "session destroyed");
-            mController.unregisterCallback(mSessionCallback);
-
-            // Hide all the old buttons
-            final int[] actionIds = {
-                    R.id.action0,
-                    R.id.action1,
-                    R.id.action2,
-                    R.id.action3,
-                    R.id.action4
-            };
-            for (int i = 0; i < actionIds.length; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-                if (thisBtn != null) {
-                    thisBtn.setVisibility(View.GONE);
-                }
-            }
-
-            // Add a restart button
-            ImageButton btn = mMediaNotifView.findViewById(actionIds[0]);
-            btn.setOnClickListener(v -> {
-                Log.d(TAG, "Attempting to restart session");
-                // Send a media button event to previously found receiver
-                if (mRecvComponent != null) {
-                    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                    intent.setComponent(mRecvComponent);
-                    int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
-                    intent.putExtra(
-                            Intent.EXTRA_KEY_EVENT,
-                            new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
-                    mContext.sendBroadcast(intent);
-                } else {
-                    Log.d(TAG, "No receiver to restart");
-                    // If we don't have a receiver, try relaunching the activity instead
-                    try {
-                        mController.getSessionActivity().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Pending intent was canceled");
-                        e.printStackTrace();
-                    }
-                }
-            });
-            btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
-            btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
-            btn.setVisibility(View.VISIBLE);
-
-            // Add long-click option to remove the player
-            ViewGroup mMediaCarousel = (ViewGroup) mMediaNotifView.getParent();
-            mMediaNotifView.setOnLongClickListener(v -> {
-                // Replace player view with delete/cancel view
-                v.setVisibility(View.GONE);
-
-                View options = LayoutInflater.from(mContext).inflate(
-                        R.layout.qs_media_panel_options, null, false);
-                ImageButton btnDelete = options.findViewById(R.id.remove);
-                btnDelete.setOnClickListener(b -> {
-                    mMediaCarousel.removeView(options);
-                    mParent.removeMediaPlayer(QSMediaPlayer.this);
-                });
-                ImageButton btnCancel = options.findViewById(R.id.cancel);
-                btnCancel.setOnClickListener(b -> {
-                    mMediaCarousel.removeView(options);
-                    v.setVisibility(View.VISIBLE);
-                });
-
-                int pos = mMediaCarousel.indexOfChild(v);
-                mMediaCarousel.addView(options, pos, v.getLayoutParams());
-                return true; // consumed click
-            });
-        }
+    // 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
     };
 
     /**
-     *
+     * Initialize quick shade version of player
      * @param context
      * @param parent
+     * @param manager
+     * @param backgroundExecutor
      */
-    public QSMediaPlayer(Context context, ViewGroup parent) {
-        mContext = context;
-        LayoutInflater inflater = LayoutInflater.from(mContext);
-        mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
-    }
-
-    public View getView() {
-        return mMediaNotifView;
+    public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
+            Executor backgroundExecutor) {
+        super(context, parent, manager, R.layout.qs_media_panel, QS_ACTION_IDS, backgroundExecutor);
     }
 
     /**
-     * Create or update the player view for the given media session
-     * @param parent the parent QSPanel
+     * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
      * @param iconColor foreground color (for text, icons)
@@ -177,114 +72,20 @@
      * @param notif reference to original notification
      * @param device current playback device
      */
-    public void setMediaSession(QSPanel parent, MediaSession.Token token, Icon icon, int iconColor,
+    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
             int bgColor, View actionsContainer, Notification notif, MediaDevice device) {
-        mParent = parent;
-        mToken = token;
-        mForegroundColor = iconColor;
-        mBackgroundColor = bgColor;
-        mController = new MediaController(mContext, token);
 
-        // Try to find a receiver for the media button that matches this app
-        PackageManager pm = mContext.getPackageManager();
-        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
-        if (info != null) {
-            for (ResolveInfo inf : info) {
-                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
-                    mRecvComponent = inf.getComponentInfo().getComponentName();
-                }
-            }
-        }
-
-        // reset in case we had previously restarted the stream
-        mMediaNotifView.setOnLongClickListener(null);
-        mController.registerCallback(mSessionCallback);
-        MediaMetadata mMediaMetadata = mController.getMetadata();
-        if (mMediaMetadata == null) {
-            Log.e(TAG, "Media metadata was null");
-            return;
-        }
-
-        Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif);
-
-        // Album art
-        addAlbumArt(mMediaMetadata, bgColor);
-
-        LinearLayout headerView = mMediaNotifView.findViewById(R.id.header);
-
-        // App icon
-        ImageView appIcon = headerView.findViewById(R.id.icon);
-        Drawable iconDrawable = icon.loadDrawable(mContext);
-        iconDrawable.setTint(iconColor);
-        appIcon.setImageDrawable(iconDrawable);
-
-        // App title
-        TextView appName = headerView.findViewById(R.id.app_name);
-        String appNameString = builder.loadHeaderAppName();
-        appName.setText(appNameString);
-        appName.setTextColor(iconColor);
-
-        // Action
-        mMediaNotifView.setOnClickListener(v -> {
-            try {
-                notif.contentIntent.send();
-                // Also close shade
-                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-            } catch (PendingIntent.CanceledException e) {
-                Log.e(TAG, "Pending intent was canceled");
-                e.printStackTrace();
-            }
-        });
-
-        // Transfer chip
-        mSeamless = headerView.findViewById(R.id.media_seamless);
-        mSeamless.setVisibility(View.VISIBLE);
-        updateChip(device);
-        ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
-        mSeamless.setOnClickListener(v -> {
-            final Intent intent = new Intent()
-                    .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
-                    .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
-                        mController.getPackageName())
-                    .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, token);
-            mActivityStarter.startActivity(intent, false, true /* dismissShade */,
-                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        });
-
-        // Artist name
-        TextView artistText = headerView.findViewById(R.id.header_artist);
-        String artistName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
-        artistText.setText(artistName);
-        artistText.setTextColor(iconColor);
-
-        // Song name
-        TextView titleText = headerView.findViewById(R.id.header_text);
-        String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        titleText.setText(songName);
-        titleText.setTextColor(iconColor);
+        String appName = Notification.Builder.recoverBuilder(getContext(), notif)
+                .loadHeaderAppName();
+        super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent,
+                appName, device);
 
         // Media controls
         LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        final int[] actionIds = {
-                R.id.action0,
-                R.id.action1,
-                R.id.action2,
-                R.id.action3,
-                R.id.action4
-        };
-        final int[] notifActionIds = {
-                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
-        };
-
         int i = 0;
-        for (; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-            ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]);
+        for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
+            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) {
                 thisBtn.setVisibility(View.GONE);
@@ -301,116 +102,9 @@
         }
 
         // Hide any unused buttons
-        for (; i < actionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
+        for (; i < QS_ACTION_IDS.length; i++) {
+            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
             thisBtn.setVisibility(View.GONE);
         }
     }
-
-    public MediaSession.Token getMediaSessionToken() {
-        return mToken;
-    }
-
-    public String getMediaPlayerPackage() {
-        return mController.getPackageName();
-    }
-
-    /**
-     * Check whether the media controlled by this player is currently playing
-     * @return whether it is playing, or false if no controller information
-     */
-    public boolean isPlaying() {
-        if (mController == null) {
-            return false;
-        }
-
-        PlaybackState state = mController.getPlaybackState();
-        if (state == null) {
-            return false;
-        }
-
-        return (state.getState() == PlaybackState.STATE_PLAYING);
-    }
-
-    private void addAlbumArt(MediaMetadata metadata, int bgColor) {
-        Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
-        float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
-        if (albumArt != null) {
-            Log.d(TAG, "updating album art");
-            Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
-            int albumSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_album_size);
-            Bitmap scaled = scaleBitmap(original, albumSize, albumSize);
-            RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
-                    mContext.getResources(), scaled);
-            roundedDrawable.setCornerRadius(radius);
-            albumView.setImageDrawable(roundedDrawable);
-        } else {
-            Log.e(TAG, "No album art available");
-            albumView.setImageDrawable(null);
-        }
-
-        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(bgColor));
-    }
-
-    private Bitmap scaleBitmap(Bitmap original, int width, int height) {
-        Bitmap cropped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(cropped);
-
-        float scale = (float) cropped.getWidth() / (float) original.getWidth();
-        float dy = (cropped.getHeight() - original.getHeight() * scale) / 2.0f;
-        Matrix transformation = new Matrix();
-        transformation.postTranslate(0, dy);
-        transformation.preScale(scale, scale);
-
-        Paint paint = new Paint();
-        paint.setFilterBitmap(true);
-        canvas.drawBitmap(original, transformation, paint);
-
-        return cropped;
-    }
-
-    protected void updateChip(MediaDevice device) {
-        if (mSeamless == null) {
-            return;
-        }
-        Handler handler = mSeamless.getHandler();
-        handler.post(() -> {
-            updateChipInternal(device);
-        });
-    }
-
-    private void updateChipInternal(MediaDevice device) {
-        ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
-
-        // Update the outline color
-        LinearLayout viewLayout = (LinearLayout) mSeamless;
-        RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
-        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
-        rect.setStroke(2, mForegroundColor);
-        rect.setColor(mBackgroundColor);
-
-        ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
-        TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
-        deviceName.setTextColor(fgTintList);
-
-        if (device != null) {
-            Drawable icon = device.getIcon();
-            iconView.setVisibility(View.VISIBLE);
-            iconView.setImageTintList(fgTintList);
-
-            if (icon instanceof AdaptiveIcon) {
-                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
-                aIcon.setBackgroundColor(mBackgroundColor);
-                iconView.setImageDrawable(aIcon);
-            } else {
-                iconView.setImageDrawable(icon);
-            }
-            deviceName.setText(device.getName());
-        } else {
-            // Reset to default
-            iconView.setVisibility(View.GONE);
-            deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index d2d9092..9ab4714 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -50,6 +50,7 @@
 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.dump.DumpManager;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSTile;
@@ -60,6 +61,7 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.BrightnessController;
 import com.android.systemui.settings.ToggleSliderView;
+import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
 import com.android.systemui.tuner.TunerService;
@@ -70,6 +72,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -94,6 +97,8 @@
 
     private final LinearLayout mMediaCarousel;
     private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
+    private final NotificationMediaManager mNotificationMediaManager;
+    private final Executor mBackgroundExecutor;
     private LocalMediaManager mLocalMediaManager;
     private MediaDevice mDevice;
     private boolean mUpdateCarousel = false;
@@ -128,7 +133,7 @@
             if (mDevice == null || !mDevice.equals(currentDevice)) {
                 mDevice = currentDevice;
                 for (QSMediaPlayer p : mMediaPlayers) {
-                    p.updateChip(mDevice);
+                    p.updateDevice(mDevice);
                 }
             }
         }
@@ -138,7 +143,7 @@
             if (mDevice == null || !mDevice.equals(device)) {
                 mDevice = device;
                 for (QSMediaPlayer p : mMediaPlayers) {
-                    p.updateChip(mDevice);
+                    p.updateDevice(mDevice);
                 }
             }
         }
@@ -150,12 +155,16 @@
             AttributeSet attrs,
             DumpManager dumpManager,
             BroadcastDispatcher broadcastDispatcher,
-            QSLogger qsLogger
+            QSLogger qsLogger,
+            NotificationMediaManager notificationMediaManager,
+            @Background Executor backgroundExecutor
     ) {
         super(context, attrs);
         mContext = context;
         mQSLogger = qsLogger;
         mDumpManager = dumpManager;
+        mNotificationMediaManager = notificationMediaManager;
+        mBackgroundExecutor = backgroundExecutor;
 
         setOrientation(VERTICAL);
 
@@ -255,7 +264,8 @@
 
         if (player == null) {
             Log.d(TAG, "creating new player");
-            player = new QSMediaPlayer(mContext, this);
+            player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
+                    mBackgroundExecutor);
 
             if (player.isPlaying()) {
                 mMediaCarousel.addView(player.getView(), 0, lp); // add in front
@@ -268,7 +278,7 @@
         }
 
         Log.d(TAG, "setting player session");
-        player.setMediaSession(this, token, icon, iconColor, bgColor, actionsContainer,
+        player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
                 notif.getNotification(), mDevice);
 
         if (mMediaPlayers.size() > 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
index 9018a37..4512afb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
@@ -17,108 +17,47 @@
 package com.android.systemui.qs;
 
 import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.ColorStateList;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
-import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import com.android.systemui.R;
+import com.android.systemui.media.MediaControlPanel;
+import com.android.systemui.statusbar.NotificationMediaManager;
 
-import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * QQS mini media player
  */
-public class QuickQSMediaPlayer {
+public class QuickQSMediaPlayer extends MediaControlPanel {
 
     private static final String TAG = "QQSMediaPlayer";
 
-    private Context mContext;
-    private LinearLayout mMediaNotifView;
-    private MediaSession.Token mToken;
-    private MediaController mController;
-    private int mForegroundColor;
-    private ComponentName mRecvComponent;
-
-    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
-        @Override
-        public void onSessionDestroyed() {
-            Log.d(TAG, "session destroyed");
-            mController.unregisterCallback(mSessionCallback);
-
-            // Hide all the old buttons
-            final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2};
-            for (int i = 0; i < actionIds.length; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-                if (thisBtn != null) {
-                    thisBtn.setVisibility(View.GONE);
-                }
-            }
-
-            // Add a restart button
-            ImageButton btn = mMediaNotifView.findViewById(actionIds[0]);
-            btn.setOnClickListener(v -> {
-                Log.d(TAG, "Attempting to restart session");
-                // Send a media button event to previously found receiver
-                if (mRecvComponent != null) {
-                    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                    intent.setComponent(mRecvComponent);
-                    int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
-                    intent.putExtra(
-                            Intent.EXTRA_KEY_EVENT,
-                            new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
-                    mContext.sendBroadcast(intent);
-                } else {
-                    Log.d(TAG, "No receiver to restart");
-                    // If we don't have a receiver, try relaunching the activity instead
-                    try {
-                        mController.getSessionActivity().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Pending intent was canceled");
-                        e.printStackTrace();
-                    }
-                }
-            });
-            btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
-            btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
-            btn.setVisibility(View.VISIBLE);
-        }
-    };
+    // 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 manager
+     * @param backgroundExecutor
      */
-    public QuickQSMediaPlayer(Context context, ViewGroup parent) {
-        mContext = context;
-        LayoutInflater inflater = LayoutInflater.from(mContext);
-        mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qqs_media_panel, parent, false);
-    }
-
-    public View getView() {
-        return mMediaNotifView;
+    public QuickQSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
+            Executor backgroundExecutor) {
+        super(context, parent, manager, R.layout.qqs_media_panel, QQS_ACTION_IDS,
+                backgroundExecutor);
     }
 
     /**
-     *
+     * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
      * @param iconColor foreground color (for text, icons)
@@ -130,84 +69,30 @@
      */
     public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
             View actionsContainer, int[] actionsToShow, PendingIntent contentIntent) {
-        mToken = token;
-        mForegroundColor = iconColor;
-
+        // Only update if this is a different session and currently playing
         String oldPackage = "";
-        if (mController != null) {
-            oldPackage = mController.getPackageName();
+        if (getController() != null) {
+            oldPackage = getController().getPackageName();
         }
-        MediaController controller = new MediaController(mContext, token);
-        boolean samePlayer = mToken.equals(token) && oldPackage.equals(controller.getPackageName());
-        if (mController != null && !samePlayer && !isPlaying(controller)) {
-            // Only update if this is a different session and currently playing
-            return;
-        }
-        mController = controller;
-        MediaMetadata mMediaMetadata = mController.getMetadata();
-
-        // Try to find a receiver for the media button that matches this app
-        PackageManager pm = mContext.getPackageManager();
-        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
-        if (info != null) {
-            for (ResolveInfo inf : info) {
-                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
-                    mRecvComponent = inf.getComponentInfo().getComponentName();
-                }
-            }
-        }
-        mController.registerCallback(mSessionCallback);
-
-        if (mMediaMetadata == null) {
-            Log.e(TAG, "Media metadata was null");
+        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;
         }
 
-        // Action
-        mMediaNotifView.setOnClickListener(v -> {
-            try {
-                contentIntent.send();
-                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-            } catch (PendingIntent.CanceledException e) {
-                Log.e(TAG, "Pending intent was canceled: " + e.getMessage());
-            }
-        });
+        super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, null, null);
 
-        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(bgColor));
-
-        // App icon
-        ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
-        Drawable iconDrawable = icon.loadDrawable(mContext);
-        iconDrawable.setTint(mForegroundColor);
-        appIcon.setImageDrawable(iconDrawable);
-
-        // Song name
-        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
-        String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        titleText.setText(songName);
-        titleText.setTextColor(mForegroundColor);
-
-        // Buttons we can display
-        final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2};
-
-        // Existing buttons in the notification
         LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        final int[] notifActionIds = {
-                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
-        };
-
         int i = 0;
         if (actionsToShow != null) {
             int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount());
-            maxButtons = Math.min(maxButtons, actionIds.length);
+            maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length);
             for (; i < maxButtons; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-                int thatId = notifActionIds[actionsToShow[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) {
@@ -225,38 +110,9 @@
         }
 
         // Hide any unused buttons
-        for (; i < actionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
+        for (; i < QQS_ACTION_IDS.length; i++) {
+            ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]);
             thisBtn.setVisibility(View.GONE);
         }
     }
-
-    public MediaSession.Token getMediaSessionToken() {
-        return mToken;
-    }
-
-    /**
-     * Check whether the media controlled by this player is currently playing
-     * @return whether it is playing, or false if no controller information
-     */
-    public boolean isPlaying(MediaController controller) {
-        if (controller == null) {
-            return false;
-        }
-
-        PlaybackState state = controller.getPlaybackState();
-        if (state == null) {
-            return false;
-        }
-
-        return (state.getState() == PlaybackState.STATE_PLAYING);
-    }
-
-    /**
-     * Check whether this player has an attached media session.
-     * @return whether there is a controller with a current media session.
-     */
-    public boolean hasMediaSession() {
-        return mController != null && mController.getPlaybackState() != null;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 20efbcb..3da767e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -29,18 +29,21 @@
 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.dump.DumpManager;
 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.NotificationMediaManager;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.systemui.util.Utils;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -72,9 +75,12 @@
             AttributeSet attrs,
             DumpManager dumpManager,
             BroadcastDispatcher broadcastDispatcher,
-            QSLogger qsLogger
+            QSLogger qsLogger,
+            NotificationMediaManager notificationMediaManager,
+            @Background Executor backgroundExecutor
     ) {
-        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger);
+        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager,
+                backgroundExecutor);
         if (mFooter != null) {
             removeView(mFooter.getView());
         }
@@ -93,7 +99,8 @@
             mHorizontalLinearLayout.setClipToPadding(false);
 
             int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing);
-            mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout);
+            mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout,
+                    notificationMediaManager, backgroundExecutor);
             LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
             lp2.setMarginEnd(marginSize);
             lp2.setMarginStart(0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
index 45c0cdd..616399a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
@@ -42,6 +42,7 @@
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.statusbar.NotificationMediaManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,6 +54,7 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Collections;
+import java.util.concurrent.Executor;
 
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
@@ -79,6 +81,10 @@
     private QSDetail.Callback mCallback;
     @Mock
     private QSTileView mQSTileView;
+    @Mock
+    private NotificationMediaManager mNotificationMediaManager;
+    @Mock
+    private Executor mBackgroundExecutor;
 
     @Before
     public void setup() throws Exception {
@@ -88,7 +94,7 @@
         mTestableLooper.runWithLooper(() -> {
             mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class);
             mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher,
-                    mQSLogger);
+                    mQSLogger, mNotificationMediaManager, mBackgroundExecutor);
             // Provides a parent with non-zero size for QSPanel
             mParentView = new FrameLayout(mContext);
             mParentView.addView(mQsPanel);