Towards paged layout: introduce view holder

Bug: 153170704
Test: Check that player + correct elements appear in QS, QQS and LS.
Change-Id: Id4792f263b405cb4e2f4cb424443db03bacd0a34
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 60c2ed2..c3a7d9f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -37,9 +37,7 @@
 import android.media.session.PlaybackState;
 import android.service.media.MediaBrowserService;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -90,16 +88,14 @@
     };
 
     private final SeekBarViewModel mSeekBarViewModel;
-    private final SeekBarObserver mSeekBarObserver;
+    private SeekBarObserver mSeekBarObserver;
     private final Executor mForegroundExecutor;
     protected final Executor mBackgroundExecutor;
     private final ActivityStarter mActivityStarter;
-    private final LayoutAnimationHelper mLayoutAnimationHelper;
+    private LayoutAnimationHelper mLayoutAnimationHelper;
 
     private Context mContext;
-    private MotionLayout mMediaNotifView;
-    private final View mBackground;
-    private View mSeamless;
+    private PlayerViewHolder mViewHolder;
     private MediaSession.Token mToken;
     private MediaController mController;
     private int mForegroundColor;
@@ -107,7 +103,7 @@
     private MediaDevice mDevice;
     protected ComponentName mServiceComponent;
     private boolean mIsRegistered = false;
-    private final List<KeyFrames> mKeyFrames;
+    private List<KeyFrames> mKeyFrames;
     private String mKey;
     private int mAlbumArtSize;
     private int mAlbumArtRadius;
@@ -166,37 +162,27 @@
     /**
      * Initialize a new control panel
      * @param context
-     * @param parent
      * @param routeManager Manager used to listen for device change events.
      * @param foregroundExecutor foreground executor
      * @param backgroundExecutor background executor, used for processing artwork
      * @param activityStarter activity starter
      */
-    public MediaControlPanel(Context context, ViewGroup parent,
-            @Nullable LocalMediaManager routeManager, Executor foregroundExecutor,
-            DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
+    public MediaControlPanel(Context context, @Nullable LocalMediaManager routeManager,
+            Executor foregroundExecutor, DelayableExecutor backgroundExecutor,
+            ActivityStarter activityStarter) {
         mContext = context;
-        LayoutInflater inflater = LayoutInflater.from(mContext);
-        mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
-        mBackground = mMediaNotifView.findViewById(R.id.media_background);
-        mLayoutAnimationHelper = new LayoutAnimationHelper(mMediaNotifView);
-        GoneChildrenHideHelper.clipGoneChildrenOnLayout(mMediaNotifView);
-        mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
         mLocalMediaManager = routeManager;
         mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mActivityStarter = activityStarter;
         mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
-        mSeekBarObserver = new SeekBarObserver(getView());
-        mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
-        SeekBar bar = getView().findViewById(R.id.media_progress_bar);
-        bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
-        bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
         loadDimens();
     }
 
     public void onDestroy() {
-        mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
+        if (mSeekBarObserver != null) {
+            mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
+        }
         makeInactive();
     }
 
@@ -207,11 +193,12 @@
     }
 
     /**
-     * Get the view used to display media controls
-     * @return the view
+     * Get the view holder used to display media controls
+     * @return the view holder
      */
-    public MotionLayout getView() {
-        return mMediaNotifView;
+    @Nullable
+    public PlayerViewHolder getView() {
+        return mViewHolder;
     }
 
     /**
@@ -234,10 +221,27 @@
         return mContext;
     }
 
+    /** Attaches the player to the view holder. */
+    public void attach(PlayerViewHolder vh) {
+        mViewHolder = vh;
+        MotionLayout motionView = vh.getPlayer();
+        mLayoutAnimationHelper = new LayoutAnimationHelper(motionView);
+        GoneChildrenHideHelper.clipGoneChildrenOnLayout(motionView);
+        mKeyFrames = motionView.getDefinedTransitions().get(0).getKeyFrameList();
+        mSeekBarObserver = new SeekBarObserver(motionView);
+        mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
+        SeekBar bar = vh.getSeekBar();
+        bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
+        bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
+    }
+
     /**
      * Bind this view based on the data given
      */
     public void bind(@NotNull MediaData data) {
+        if (mViewHolder == null) {
+            return;
+        }
         MediaSession.Token token = data.getToken();
         mForegroundColor = data.getForegroundColor();
         mBackgroundColor = data.getBackgroundColor();
@@ -254,8 +258,8 @@
 
         mController = new MediaController(mContext, mToken);
 
-        ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
-        ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
+        ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
+        ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
 
         // Try to find a browser service component for this app
         // TODO also check for a media button receiver intended for restarting (b/154127084)
@@ -281,64 +285,61 @@
 
         mController.registerCallback(mSessionCallback);
 
-        mMediaNotifView.requireViewById(R.id.media_background).setBackgroundTintList(
+        mViewHolder.getBackground().setBackgroundTintList(
                 ColorStateList.valueOf(mBackgroundColor));
 
         // Click action
         PendingIntent clickIntent = data.getClickIntent();
         if (clickIntent != null) {
-            mMediaNotifView.setOnClickListener(v -> {
+            mViewHolder.getPlayer().setOnClickListener(v -> {
                 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
             });
         }
 
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        ImageView albumView = mViewHolder.getAlbumView();
         // TODO: migrate this to a view with rounded corners instead of baking the rounding
         // into the bitmap
         Drawable artwork = createRoundedBitmap(data.getArtwork());
         albumView.setImageDrawable(artwork);
 
         // App icon
-        ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
+        ImageView appIcon = mViewHolder.getAppIcon();
         Drawable iconDrawable = data.getAppIcon().mutate();
         iconDrawable.setTint(mForegroundColor);
         appIcon.setImageDrawable(iconDrawable);
 
         // Song name
-        TextView titleText = mMediaNotifView.requireViewById(R.id.header_title);
+        TextView titleText = mViewHolder.getTitleText();
         titleText.setText(data.getSong());
         titleText.setTextColor(data.getForegroundColor());
 
         // App title
-        TextView appName = mMediaNotifView.requireViewById(R.id.app_name);
+        TextView appName = mViewHolder.getAppName();
         appName.setText(data.getApp());
         appName.setTextColor(mForegroundColor);
 
         // Artist name
-        TextView artistText = mMediaNotifView.requireViewById(R.id.header_artist);
+        TextView artistText = mViewHolder.getArtistText();
         artistText.setText(data.getArtist());
         artistText.setTextColor(mForegroundColor);
 
         // Transfer chip
-        mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
-        if (mSeamless != null) {
-            if (mLocalMediaManager != null) {
-                mSeamless.setVisibility(View.VISIBLE);
-                setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
-                setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
-                updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
-                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);
-                });
-            } else {
-                Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
-            }
+        if (mLocalMediaManager != null) {
+            mViewHolder.getSeamless().setVisibility(View.VISIBLE);
+            setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
+            setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
+            updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
+            mViewHolder.getSeamless().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);
+            });
+        } else {
+            Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
         }
         PlaybackInfo playbackInfo = mController.getPlaybackInfo();
         if (playbackInfo != null) {
@@ -353,16 +354,16 @@
         List<MediaAction> actionIcons = data.getActions();
         for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
             int actionId = ACTION_IDS[i];
-            final ImageButton button = mMediaNotifView.findViewById(actionId);
+            final ImageButton button = mViewHolder.getAction(actionId);
             MediaAction mediaAction = actionIcons.get(i);
             button.setImageDrawable(mediaAction.getDrawable());
             button.setContentDescription(mediaAction.getContentDescription());
             button.setImageTintList(ColorStateList.valueOf(mForegroundColor));
             PendingIntent actionIntent = mediaAction.getIntent();
 
-            if (mBackground.getBackground() instanceof IlluminationDrawable) {
-                ((IlluminationDrawable) mBackground.getBackground())
-                        .setupTouch(button, mMediaNotifView);
+            if (mViewHolder.getBackground().getBackground() instanceof IlluminationDrawable) {
+                ((IlluminationDrawable) mViewHolder.getBackground().getBackground())
+                        .setupTouch(button, mViewHolder.getPlayer());
             }
 
             button.setOnClickListener(v -> {
@@ -397,8 +398,8 @@
         makeActive();
 
         // Update both constraint sets to regenerate the animation.
-        mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
-        mMediaNotifView.updateState(R.id.expanded, expandedSet);
+        mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
+        mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
     }
 
     @UiThread
@@ -441,6 +442,9 @@
      * @param visible is the view visible
      */
     private void updateKeyFrameVisibility(int actionId, boolean visible) {
+        if (mKeyFrames == null) {
+            return;
+        }
         for (int i = 0; i < mKeyFrames.size(); i++) {
             KeyFrames keyframe = mKeyFrames.get(i);
             ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
@@ -528,38 +532,38 @@
      * @param device device information to display
      */
     private void updateDevice(MediaDevice device) {
-        if (mSeamless == null) {
-            return;
-        }
         mForegroundExecutor.execute(() -> {
             updateChipInternal(device);
         });
     }
 
     private void updateChipInternal(MediaDevice device) {
+        if (mViewHolder == null) {
+            return;
+        }
         ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
 
         // Update the outline color
-        LinearLayout viewLayout = (LinearLayout) mSeamless;
+        LinearLayout viewLayout = (LinearLayout) mViewHolder.getSeamless();
         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);
+        ImageView iconView = mViewHolder.getSeamlessIcon();
+        TextView deviceName = mViewHolder.getSeamlessText();
         deviceName.setTextColor(fgTintList);
 
         if (mIsRemotePlayback) {
-            mSeamless.setEnabled(false);
-            mSeamless.setAlpha(0.38f);
+            mViewHolder.getSeamless().setEnabled(false);
+            mViewHolder.getSeamless().setAlpha(0.38f);
             iconView.setImageResource(R.drawable.ic_hardware_speaker);
             iconView.setVisibility(View.VISIBLE);
             iconView.setImageTintList(fgTintList);
             deviceName.setText(R.string.media_seamless_remote_device);
         } else if (device != null) {
-            mSeamless.setEnabled(true);
-            mSeamless.setAlpha(1f);
+            mViewHolder.getSeamless().setEnabled(true);
+            mViewHolder.getSeamless().setAlpha(1f);
             Drawable icon = device.getIcon();
             iconView.setVisibility(View.VISIBLE);
             iconView.setImageTintList(fgTintList);
@@ -575,8 +579,8 @@
         } else {
             // Reset to default
             Log.d(TAG, "device is null. Not binding output chip.");
-            mSeamless.setEnabled(true);
-            mSeamless.setAlpha(1f);
+            mViewHolder.getSeamless().setEnabled(true);
+            mViewHolder.getSeamless().setAlpha(1f);
             iconView.setVisibility(View.GONE);
             deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
         }
@@ -601,17 +605,20 @@
      * Hide the media buttons and show only a restart button
      */
     protected void resetButtons() {
+        if (mViewHolder == null) {
+            return;
+        }
         // Hide all the old buttons
 
-        ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
-        ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
+        ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
+        ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
         for (int i = 1; i < ACTION_IDS.length; i++) {
             setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
             setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
         }
 
         // Add a restart button
-        ImageButton btn = mMediaNotifView.findViewById(ACTION_IDS[0]);
+        ImageButton btn = mViewHolder.getAction0();
         btn.setOnClickListener(v -> {
             Log.d(TAG, "Attempting to restart session");
             if (mQSMediaBrowser != null) {
@@ -639,9 +646,9 @@
         mSeekBarViewModel.clearController();
         // TODO: fix guts
         //        View guts = mMediaNotifView.findViewById(R.id.media_guts);
-        View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
+        View options = mViewHolder.getOptions();
 
-        mMediaNotifView.setOnLongClickListener(v -> {
+        mViewHolder.getPlayer().setOnLongClickListener(v -> {
             // Replace player view with close/cancel view
 //            guts.setVisibility(View.GONE);
             options.setVisibility(View.VISIBLE);
@@ -748,20 +755,28 @@
     protected void removePlayer() { }
 
     public void measure(@Nullable MediaMeasurementInput input) {
+        if (mViewHolder == null) {
+            return;
+        }
         if (input != null) {
             int width = input.getWidth();
             setPlayerWidth(width);
-            mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec());
+            mViewHolder.getPlayer().measure(input.getWidthMeasureSpec(),
+                    input.getHeightMeasureSpec());
         }
     }
 
     public void setPlayerWidth(int width) {
-        ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
-        ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
+        if (mViewHolder == null) {
+            return;
+        }
+        MotionLayout view = mViewHolder.getPlayer();
+        ConstraintSet expandedSet = view.getConstraintSet(R.id.expanded);
+        ConstraintSet collapsedSet = view.getConstraintSet(R.id.collapsed);
         collapsedSet.setGuidelineBegin(R.id.view_width, width);
         expandedSet.setGuidelineBegin(R.id.view_width, width);
-        mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
-        mMediaNotifView.updateState(R.id.expanded, expandedSet);
+        view.updateState(R.id.collapsed, collapsedSet);
+        view.updateState(R.id.expanded, expandedSet);
     }
 
     public void animatePendingSizeChange(long duration, long startDelay) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 49d2d88..9c92b0a2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -56,8 +56,13 @@
             }
         }
     private val scrollChangedListener = object : View.OnScrollChangeListener {
-        override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int,
-                                    oldScrollY: Int) {
+        override fun onScrollChange(
+            v: View?,
+            scrollX: Int,
+            scrollY: Int,
+            oldScrollX: Int,
+            oldScrollY: Int
+        ) {
             if (playerWidthPlusPadding == 0) {
                 return
             }
@@ -79,16 +84,17 @@
             override fun onMediaDataRemoved(key: String) {
                 val removed = mediaPlayers.remove(key)
                 removed?.apply {
-                    val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex
-                    mediaContent.removeView(removed.view)
+                    val beforeActive = mediaContent.indexOfChild(removed.view?.player) <=
+                            activeMediaIndex
+                    mediaContent.removeView(removed.view?.player)
                     removed.onDestroy()
                     updateMediaPaddings()
                     if (beforeActive) {
                         // also update the index here since the scroll below might not always lead
                         // to a scrolling changed
                         activeMediaIndex = Math.max(0, activeMediaIndex - 1)
-                        mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX
-                                - playerWidthPlusPadding, 0)
+                        mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX -
+                                playerWidthPlusPadding, 0)
                     }
                     updatePlayerVisibilities()
                 }
@@ -103,7 +109,7 @@
 
     private fun reorderAllPlayers() {
         for (mediaPlayer in mediaPlayers.values) {
-            val view = mediaPlayer.view
+            val view = mediaPlayer.view?.player
             if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) {
                 mediaContent.removeView(view)
                 mediaContent.addView(view, 0)
@@ -142,24 +148,26 @@
             val routeManager = LocalMediaManager(context, localBluetoothManager,
                     imm, data.packageName)
 
-            existingPlayer = MediaControlPanel(context, mediaContent, routeManager,
-                    foregroundExecutor, backgroundExecutor, activityStarter)
+            existingPlayer = MediaControlPanel(context, routeManager, foregroundExecutor,
+                    backgroundExecutor, activityStarter)
+            existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
+                    mediaContent))
             mediaPlayers[key] = existingPlayer
             val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                     ViewGroup.LayoutParams.WRAP_CONTENT)
-            existingPlayer.view.setLayoutParams(lp)
+            existingPlayer.view?.player?.setLayoutParams(lp)
             existingPlayer.setListening(currentlyExpanded)
             if (existingPlayer.isPlaying) {
-                mediaContent.addView(existingPlayer.view, 0)
+                mediaContent.addView(existingPlayer.view?.player, 0)
             } else {
-                mediaContent.addView(existingPlayer.view)
+                mediaContent.addView(existingPlayer.view?.player)
             }
             updatePlayerToCurrentState(existingPlayer)
         } else if (existingPlayer.isPlaying &&
-                    mediaContent.indexOfChild(existingPlayer.view) != 0) {
+                    mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
             if (visualStabilityManager.isReorderingAllowed) {
-                mediaContent.removeView(existingPlayer.view)
-                mediaContent.addView(existingPlayer.view, 0)
+                mediaContent.removeView(existingPlayer.view?.player)
+                mediaContent.addView(existingPlayer.view?.player, 0)
             } else {
                 visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback)
             }
@@ -167,7 +175,7 @@
         existingPlayer.bind(data)
         // Resetting the progress to make sure it's taken into account for the latest
         // motion model
-        existingPlayer.view.progress = currentState?.expansion ?: 0.0f
+        existingPlayer.view?.player?.progress = currentState?.expansion ?: 0.0f
         updateMediaPaddings()
     }
 
@@ -190,7 +198,6 @@
                 mediaView.layoutParams = layoutParams
             }
         }
-
     }
 
     /**
@@ -201,8 +208,8 @@
         currentState = state
         currentlyExpanded = state.expansion > 0
         for (mediaPlayer in mediaPlayers.values) {
-            val view = mediaPlayer.view
-            view.progress = state.expansion
+            val view = mediaPlayer.view?.player
+            view?.progress = state.expansion
         }
     }
 
@@ -215,8 +222,12 @@
      * @param desiredState the target state we're transitioning to
      * @param animate should this be animated
      */
-    fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long,
-                                 startDelay: Long) {
+    fun onDesiredLocationChanged(
+        desiredState: MediaState?,
+        animate: Boolean,
+        duration: Long,
+        startDelay: Long
+    ) {
         if (desiredState is MediaHost.MediaHostState) {
             // This is a hosting view, let's remeasure our players
             this.desiredState = desiredState
@@ -224,7 +235,7 @@
             if (playerWidth != width) {
                 setPlayerWidth(width)
                 for (mediaPlayer in mediaPlayers.values) {
-                    if (animate && mediaPlayer.view.visibility == View.VISIBLE) {
+                    if (animate && mediaPlayer.view?.player?.visibility == View.VISIBLE) {
                         mediaPlayer.animatePendingSizeChange(duration, startDelay)
                     }
                 }
@@ -266,24 +277,25 @@
      * Get a measurement for the given input state. This measures the first player and returns
      * its bounds as if it were measured with the given measurement dimensions
      */
-    fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
+    fun obtainMeasurement(input: MediaMeasurementInput): MeasurementOutput? {
         val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
-        // Let's measure the size of the first player and return its height
-        val previousProgress = firstPlayer.view.progress
-        val previousRight = firstPlayer.view.right
-        val previousBottom = firstPlayer.view.bottom
-        firstPlayer.view.progress = input.expansion
-        firstPlayer.measure(input)
-        // Relayouting is necessary in motionlayout to obtain its size properly ....
-        firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth,
-                firstPlayer.view.measuredHeight)
-        val result = MeasurementOutput(firstPlayer.view.measuredWidth,
-                firstPlayer.view.measuredHeight)
-        firstPlayer.view.progress = previousProgress
-        if (desiredState != null) {
-            // remeasure it to the old size again!
-            firstPlayer.measure(desiredState!!.measurementInput)
-            firstPlayer.view.layout(0, 0, previousRight, previousBottom)
+        var result: MeasurementOutput? = null
+        firstPlayer.view?.player?.let {
+            // Let's measure the size of the first player and return its height
+            val previousProgress = it.progress
+            val previousRight = it.right
+            val previousBottom = it.bottom
+            it.progress = input.expansion
+            firstPlayer.measure(input)
+            // Relayouting is necessary in motionlayout to obtain its size properly ....
+            it.layout(0, 0, it.measuredWidth, it.measuredHeight)
+            val result = MeasurementOutput(it.measuredWidth, it.measuredHeight)
+            it.progress = previousProgress
+            if (desiredState != null) {
+                // remeasure it to the old size again!
+                firstPlayer.measure(desiredState!!.measurementInput)
+                it.layout(0, 0, previousRight, previousBottom)
+            }
         }
         return result
     }
@@ -295,8 +307,8 @@
             val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
             val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
             for (mediaPlayer in mediaPlayers.values) {
-                mediaPlayer.view.measure(widthSpec, heightSpec)
+                mediaPlayer.view?.player?.measure(widthSpec, heightSpec)
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
new file mode 100644
index 0000000..571e18d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
+
+import androidx.constraintlayout.motion.widget.MotionLayout
+
+import com.android.systemui.R
+
+/**
+ * ViewHolder for a media player.
+ */
+class PlayerViewHolder private constructor(itemView: View) {
+
+    val player = itemView as MotionLayout
+    val background = itemView.requireViewById<View>(R.id.media_background)
+
+    // Player information
+    val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
+    val appName = itemView.requireViewById<TextView>(R.id.app_name)
+    val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
+    val titleText = itemView.requireViewById<TextView>(R.id.header_title)
+    val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
+
+    // Output switcher
+    val seamless = itemView.findViewById<ViewGroup>(R.id.media_seamless)
+    val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
+    val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
+
+    // Seek bar
+    val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
+    val elapsedTimeView = itemView.requireViewById<TextView>(R.id.media_elapsed_time)
+    val totalTimeView = itemView.requireViewById<TextView>(R.id.media_total_time)
+
+    // Action Buttons
+    val action0 = itemView.requireViewById<ImageButton>(R.id.action0)
+    val action1 = itemView.requireViewById<ImageButton>(R.id.action1)
+    val action2 = itemView.requireViewById<ImageButton>(R.id.action2)
+    val action3 = itemView.requireViewById<ImageButton>(R.id.action3)
+    val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
+
+    fun getAction(id: Int): ImageButton {
+        return when (id) {
+            R.id.action0 -> action0
+            R.id.action1 -> action1
+            R.id.action2 -> action2
+            R.id.action3 -> action3
+            R.id.action4 -> action4
+            else -> {
+                throw IllegalArgumentException()
+            }
+        }
+    }
+
+    // Settings screen
+    val options = itemView.requireViewById<View>(R.id.qs_media_controls_options)
+
+    companion object {
+        /**
+         * Creates a PlayerViewHolder.
+         *
+         * @param inflater LayoutInflater to use to inflate the layout.
+         * @param parent Parent of inflated view.
+         */
+        @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): PlayerViewHolder {
+            val v = inflater.inflate(R.layout.qs_media_panel, parent, false)
+            return PlayerViewHolder(v)
+        }
+    }
+}