[Media] Ensure that we update the seekbar visibility whenever its
enabled status changes.

See b/228414210#comment4 for an in-depth explanation.

Fixes: 228414210
Test: manual: Start a media session then update the display size
or font size; verify that the seekbar still shows up.
Test: MediaControlPanelTest

Change-Id: I37ec1149ce24b81ca7f545ee524df68173b3c943
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 48a63ed..12369e5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -176,9 +176,12 @@
     private String mPackageName;
 
     private boolean mIsScrubbing = false;
+    private boolean mIsSeekBarEnabled = false;
 
     private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
             this::setIsScrubbing;
+    private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener =
+            this::setIsSeekBarEnabled;
 
     /**
      * Initialize a new control panel
@@ -235,8 +238,9 @@
     public void onDestroy() {
         if (mSeekBarObserver != null) {
             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
-            mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
         }
+        mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
+        mSeekBarViewModel.removeEnabledChangeListener(mEnabledChangeListener);
         mSeekBarViewModel.onDestroy();
         mMediaViewController.onDestroy();
     }
@@ -283,7 +287,7 @@
     }
 
     /** Sets whether the user is touching the seek bar to change the track position. */
-    public void setIsScrubbing(boolean isScrubbing) {
+    private void setIsScrubbing(boolean isScrubbing) {
         if (mMediaData == null || mMediaData.getSemanticActions() == null) {
             return;
         }
@@ -295,6 +299,14 @@
                 updateDisplayForScrubbingChange(mMediaData.getSemanticActions()));
     }
 
+    private void setIsSeekBarEnabled(boolean isSeekBarEnabled) {
+        if (isSeekBarEnabled == this.mIsSeekBarEnabled) {
+            return;
+        }
+        this.mIsSeekBarEnabled = isSeekBarEnabled;
+        updateSeekBarVisibility();
+    }
+
     /**
      * Get the context
      *
@@ -313,6 +325,7 @@
         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
         mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
         mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
+        mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener);
         mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
 
         vh.getPlayer().setOnLongClickListener(v -> {
@@ -450,8 +463,8 @@
 
         bindOutputSwitcherChip(data);
         bindLongPressMenu(data);
-        bindActionButtons(data);
         bindScrubbingTime(data);
+        bindActionButtons(data);
 
         boolean isSongUpdated = bindSongMetadata(data);
         bindArtworkAndColors(data, isSongUpdated);
@@ -735,13 +748,18 @@
                         /* showInCompact= */ false);
             }
         }
+
+        updateSeekBarVisibility();
+    }
+
+    private void updateSeekBarVisibility() {
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
         expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
-        expandedSet.setAlpha(R.id.media_progress_bar, mSeekBarViewModel.getEnabled() ? 1.0f : 0.0f);
+        expandedSet.setAlpha(R.id.media_progress_bar, mIsSeekBarEnabled ? 1.0f : 0.0f);
     }
 
     private int getSeekBarVisibility() {
-        boolean seekbarEnabled = mSeekBarViewModel.getEnabled();
-        if (seekbarEnabled) {
+        if (mIsSeekBarEnabled) {
             return ConstraintSet.VISIBLE;
         }
         // If disabled and "neighbours" are visible, set progress bar to INVISIBLE instead of GONE
@@ -751,8 +769,7 @@
 
     private boolean areAnyExpandedBottomActionsVisible() {
         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        int[] referencedIds = mMediaViewHolder.getActionsTopBarrier().getReferencedIds();
-        for (int id : referencedIds) {
+        for (int id : MediaViewHolder.Companion.getExpandedBottomActionIds()) {
             if (expandedSet.getVisibility(id) == ConstraintSet.VISIBLE) {
                 return true;
             }
@@ -872,7 +889,6 @@
     }
 
     /** Updates all the views that might change due to a scrubbing state change. */
-    // TODO(b/209656742): Handle scenarios where actionPrev and/or actionNext aren't active.
     private void updateDisplayForScrubbingChange(@NonNull MediaButton semanticActions) {
         // Update visibilities of the scrubbing time views and the scrubbing-dependent buttons.
         bindScrubbingTime(mMediaData);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
index 8964d71..b8b7318 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
@@ -187,5 +187,17 @@
             R.id.action3,
             R.id.action4
         )
+
+        val expandedBottomActionIds = setOf(
+            R.id.actionPrev,
+            R.id.actionNext,
+            R.id.action0,
+            R.id.action1,
+            R.id.action2,
+            R.id.action3,
+            R.id.action4,
+            R.id.media_scrubbing_elapsed_time,
+            R.id.media_scrubbing_total_time
+        )
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
index 5218492..193166b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
@@ -76,7 +76,11 @@
 ) {
     private var _data = Progress(false, false, false, false, null, 0)
         set(value) {
+            val enabledChanged = value.enabled != field.enabled
             field = value
+            if (enabledChanged) {
+                enabledChangeListener?.onEnabledChanged(value.enabled)
+            }
             _progress.postValue(value)
         }
     private val _progress = MutableLiveData<Progress>().apply {
@@ -122,6 +126,7 @@
         }
 
     private var scrubbingChangeListener: ScrubbingChangeListener? = null
+    private var enabledChangeListener: EnabledChangeListener? = null
 
     /** Set to true when the user is touching the seek bar to change the position. */
     private var scrubbing = false
@@ -136,8 +141,6 @@
 
     lateinit var logSeek: () -> Unit
 
-    fun getEnabled() = _data.enabled
-
     /**
      * Event indicating that the user has started interacting with the seek bar.
      */
@@ -189,6 +192,9 @@
 
     /**
      * Updates media information.
+     *
+     * This function makes a binder call, so it must happen on a worker thread.
+     *
      * @param mediaController controller for media session
      */
     @WorkerThread
@@ -232,6 +238,7 @@
         cancel?.run()
         cancel = null
         scrubbingChangeListener = null
+        enabledChangeListener = null
     }
 
     @WorkerThread
@@ -279,11 +286,26 @@
         }
     }
 
+    fun setEnabledChangeListener(listener: EnabledChangeListener) {
+        enabledChangeListener = listener
+    }
+
+    fun removeEnabledChangeListener(listener: EnabledChangeListener) {
+        if (listener == enabledChangeListener) {
+            enabledChangeListener = null
+        }
+    }
+
     /** Listener interface to be notified when the user starts or stops scrubbing. */
     interface ScrubbingChangeListener {
         fun onScrubbingChanged(scrubbing: Boolean)
     }
 
+    /** Listener interface to be notified when the seekbar's enabled status changes. */
+    interface EnabledChangeListener {
+        fun onEnabledChanged(enabled: Boolean)
+    }
+
     private class SeekBarChangeListener(
         val viewModel: SeekBarViewModel
     ) : SeekBar.OnSeekBarChangeListener {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 33db993..4e130d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -291,7 +291,7 @@
         seamlessButton = View(context)
         seamlessIcon = ImageView(context)
         seamlessText = TextView(context)
-        seekBar = SeekBar(context)
+        seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar }
         settings = ImageButton(context)
         cancel = View(context)
         cancelText = TextView(context)
@@ -539,8 +539,8 @@
     }
 
     @Test
-    fun bind_seekBarDisabled_seekBarVisibilityIsSetToInvisible() {
-        whenever(seekBarViewModel.getEnabled()).thenReturn(false)
+    fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() {
+        useRealConstraintSets()
 
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
         val semanticActions = MediaButton(
@@ -550,21 +550,84 @@
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
         player.bindPlayer(state, PACKAGE)
 
-        verify(expandedSet).setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE)
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
     }
 
     @Test
     fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToGone() {
-        whenever(seekBarViewModel.getEnabled()).thenReturn(false)
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun bind_seekBarEnabled_seekBarVisible() {
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun seekBarChangesToEnabledAfterBind_seekBarChangesToVisible() {
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToGone() {
+        useRealConstraintSets()
 
         val state = mediaData.copy(semanticActions = MediaButton())
 
         player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
         player.bindPlayer(state, PACKAGE)
 
-        verify(expandedSet).setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE)
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun seekBarChangesToDisabledAfterBind_hasActions_seekBarChangesToInvisible() {
+        useRealConstraintSets()
+
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions = MediaButton(
+            nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
+        )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+        player.bindPlayer(state, PACKAGE)
+
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
     }
 
     @Test
@@ -1317,4 +1380,24 @@
 
     private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
         withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
+
+    private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener =
+        withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) }
+
+    /**
+     *  Update our test to use real ConstraintSets instead of mocks.
+     *
+     *  Some item visibilities, such as the seekbar visibility, are dependent on other action's
+     *  visibilities. If we use mocks for the ConstraintSets, then action visibility changes are
+     *  just thrown away instead of being saved for reference later. This method sets us up to use
+     *  ConstraintSets so that we do save visibility changes.
+     *
+     *  TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
+     */
+    private fun useRealConstraintSets() {
+        expandedSet = ConstraintSet()
+        collapsedSet = ConstraintSet()
+        whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
+        whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
+    }
 }