[Media UMO] Show times when scrubbing.

Bug: 209656742
Test: manual (see video in attached bug)
Test: atest MediaControlPanelTest, SeekBarViewModelTest,
SeekBarObserverTest

Change-Id: I32687652d20715dc9d4775ec149e04e4473e9e1b
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 23d7211..b67b7bc 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -157,7 +157,7 @@
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:barrierDirection="start"
-        app:constraint_referenced_ids="actionPrev,media_progress_bar,actionNext,action0,action1,action2,action3,action4"
+        app:constraint_referenced_ids="actionPrev,media_scrubbing_elapsed_time,media_progress_bar,actionNext,media_scrubbing_total_time,action0,action1,action2,action3,action4"
         />
     <androidx.constraintlayout.widget.Barrier
         android:id="@+id/media_action_barrier_end"
@@ -167,7 +167,7 @@
         app:layout_constraintTop_toBottomOf="@id/media_seamless"
         app:layout_constraintBottom_toBottomOf="parent"
         app:barrierDirection="end"
-        app:constraint_referenced_ids="actionPrev,media_progress_bar,actionNext,action0,action1,action2,action3,action4"
+        app:constraint_referenced_ids="actionPrev,media_scrubbing_elapsed_time,media_progress_bar,actionNext,media_scrubbing_total_time,action0,action1,action2,action3,action4"
         app:layout_constraintStart_toStartOf="parent"
         />
 
@@ -177,7 +177,7 @@
         android:layout_height="0dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:barrierDirection="top"
-        app:constraint_referenced_ids="actionPrev,media_progress_bar,actionNext,action0,action1,action2,action3,action4"
+        app:constraint_referenced_ids="actionPrev,media_scrubbing_elapsed_time,media_progress_bar,actionNext,media_scrubbing_total_time,action0,action1,action2,action3,action4"
         />
 
     <!-- Button visibility will be controlled in code -->
@@ -192,6 +192,22 @@
         android:layout_marginTop="0dp"
         />
 
+    <!-- Elapsed time, shown only when scrubbing -->
+    <!-- The space to the left of the progress bar will either be actionPrev or
+         media_scrubbing_elapsed_time, so they use the same layout constraints. Visibilities of
+         elements are controlled in code. -->
+    <TextView
+        android:id="@+id/media_scrubbing_elapsed_time"
+        style="@style/MediaPlayer.ScrubbingTime"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="0dp"
+        android:layout_marginBottom="@dimen/qs_media_padding"
+        android:layout_marginTop="0dp"
+        android:visibility="gone"
+        />
+
     <!-- Seek Bar -->
     <!-- As per Material Design on Bidirectionality, this is forced to LTR in code -->
     <SeekBar
@@ -218,6 +234,22 @@
         android:layout_marginBottom="@dimen/qs_media_padding"
         android:layout_marginTop="0dp" />
 
+    <!-- Total time, shown only when scrubbing -->
+    <!-- The space to the right of the progress bar will either be actionNext or
+         media_scrubbing_total_time, so they use the same layout constraints. Visibilities of
+         elements are controlled in code. -->
+    <TextView
+        android:id="@+id/media_scrubbing_total_time"
+        style="@style/MediaPlayer.ScrubbingTime"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="0dp"
+        android:layout_marginEnd="@dimen/qs_media_action_spacing"
+        android:layout_marginBottom="@dimen/qs_media_padding"
+        android:layout_marginTop="0dp"
+        android:visibility="gone"
+        />
+
     <ImageButton
         android:id="@+id/action0"
         style="@style/MediaPlayer.SessionAction.Secondary"
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index ee77d21..f97bbee 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -601,6 +601,12 @@
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
+    <style name="MediaPlayer.ScrubbingTime">
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+        <item name="android:textSize">12sp</item>
+        <item name="android:gravity">center</item>
+    </style>
+
     <style name="MediaPlayer.Action" parent="@android:style/Widget.Material.Button.Borderless.Small">
         <item name="android:background">@drawable/qs_media_light_source</item>
         <item name="android:tint">?android:attr/textColorPrimary</item>
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index f00e031..eab7def 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -93,6 +93,11 @@
         app:layout_constraintTop_toBottomOf="@id/media_seamless"
         app:layout_constraintLeft_toRightOf="@id/media_action_barrier" />
 
+    <!-- Showing time while scrubbing isn't available in collapsed mode. -->
+    <Constraint
+        android:id="@+id/media_scrubbing_elapsed_time"
+        android:visibility="gone" />
+
     <Constraint
         android:id="@+id/media_progress_bar"
         android:layout_width="0dp"
@@ -116,6 +121,11 @@
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintTop_toBottomOf="@id/media_seamless" />
 
+    <!-- Showing time while scrubbing isn't available in collapsed mode. -->
+    <Constraint
+        android:id="@+id/media_scrubbing_total_time"
+        android:visibility="gone" />
+
     <Constraint
         android:id="@+id/action0"
         android:layout_width="48dp"
diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml
index bec4e7a..522dc68 100644
--- a/packages/SystemUI/res/xml/media_session_expanded.xml
+++ b/packages/SystemUI/res/xml/media_session_expanded.xml
@@ -68,10 +68,19 @@
     The chain is set to "spread" so that the progress bar can be weighted to fill any empty space.
      -->
     <Constraint
-        android:id="@+id/actionPrev"
+        android:id="@+id/media_scrubbing_elapsed_time"
         android:layout_width="48dp"
         android:layout_height="48dp"
         app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toLeftOf="@id/actionPrev"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHorizontal_chainStyle="spread" />
+
+    <Constraint
+        android:id="@+id/actionPrev"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        app:layout_constraintLeft_toRightOf="@id/media_scrubbing_elapsed_time"
         app:layout_constraintRight_toLeftOf="@id/media_progress_bar"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintHorizontal_chainStyle="spread" />
@@ -90,6 +99,14 @@
         android:layout_width="48dp"
         android:layout_height="48dp"
         app:layout_constraintLeft_toRightOf="@id/media_progress_bar"
+        app:layout_constraintRight_toLeftOf="@id/media_scrubbing_total_time"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <Constraint
+        android:id="@+id/media_scrubbing_total_time"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        app:layout_constraintLeft_toRightOf="@id/actionNext"
         app:layout_constraintRight_toLeftOf="@id/action0"
         app:layout_constraintBottom_toBottomOf="parent" />
 
@@ -97,7 +114,7 @@
         android:id="@+id/action0"
         android:layout_width="48dp"
         android:layout_height="48dp"
-        app:layout_constraintLeft_toRightOf="@id/actionNext"
+        app:layout_constraintLeft_toRightOf="@id/media_scrubbing_total_time"
         app:layout_constraintRight_toLeftOf="@id/action1"
         app:layout_constraintBottom_toBottomOf="parent" />
 
@@ -115,7 +132,7 @@
         android:layout_height="48dp"
         app:layout_constraintLeft_toRightOf="@id/action1"
         app:layout_constraintRight_toLeftOf="@id/action3"
-        app:layout_constraintBottom_toBottomOf="parent"/>
+        app:layout_constraintBottom_toBottomOf="parent" />
 
     <Constraint
         android:id="@+id/action3"
@@ -123,7 +140,7 @@
         android:layout_height="48dp"
         app:layout_constraintLeft_toRightOf="@id/action2"
         app:layout_constraintRight_toLeftOf="@id/action4"
-        app:layout_constraintBottom_toBottomOf="parent"/>
+        app:layout_constraintBottom_toBottomOf="parent" />
 
     <Constraint
         android:id="@+id/action4"
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 3a727ba..aac28d1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -60,6 +60,7 @@
 import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.plugins.ActivityStarter;
@@ -108,6 +109,13 @@
             R.id.actionNext
     );
 
+    // Buttons that should get hidden when we're scrubbing (they will be replaced with the views
+    // showing scrubbing time)
+    private static final List<Integer> SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = List.of(
+            R.id.actionPrev,
+            R.id.actionNext
+    );
+
     // Buttons to show in small player when using semantic actions
     private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of(
             R.id.actionPlayPause,
@@ -120,6 +128,7 @@
     private final SeekBarViewModel mSeekBarViewModel;
     private SeekBarObserver mSeekBarObserver;
     protected final Executor mBackgroundExecutor;
+    private final Executor mMainExecutor;
     private final ActivityStarter mActivityStarter;
     private final BroadcastSender mBroadcastSender;
 
@@ -127,6 +136,7 @@
     private MediaViewHolder mMediaViewHolder;
     private RecommendationViewHolder mRecommendationViewHolder;
     private String mKey;
+    private MediaData mMediaData;
     private MediaViewController mMediaViewController;
     private MediaSession.Token mToken;
     private MediaController mController;
@@ -147,14 +157,23 @@
     protected int mSmartspaceId = -1;
     private String mPackageName;
 
+    private boolean mIsScrubbing = false;
+
+    private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
+            this::setIsScrubbing;
+
     /**
      * Initialize a new control panel
      *
      * @param backgroundExecutor background executor, used for processing artwork
+     * @param mainExecutor main thread executor, used if we receive callbacks on the background
+     *                     thread that then trigger UI changes.
      * @param activityStarter    activity starter
      */
     @Inject
-    public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
+    public MediaControlPanel(Context context,
+            @Background Executor backgroundExecutor,
+            @Main Executor mainExecutor,
             ActivityStarter activityStarter, BroadcastSender broadcastSender,
             MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel,
             Lazy<MediaDataManager> lazyMediaDataManager,
@@ -163,6 +182,7 @@
             FalsingManager falsingManager, SystemClock systemClock, MediaUiEventLogger logger) {
         mContext = context;
         mBackgroundExecutor = backgroundExecutor;
+        mMainExecutor = mainExecutor;
         mActivityStarter = activityStarter;
         mBroadcastSender = broadcastSender;
         mSeekBarViewModel = seekBarViewModel;
@@ -186,6 +206,7 @@
     public void onDestroy() {
         if (mSeekBarObserver != null) {
             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
+            mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
         }
         mSeekBarViewModel.onDestroy();
         mMediaViewController.onDestroy();
@@ -232,6 +253,19 @@
         mSeekBarViewModel.setListening(listening);
     }
 
+    /** Sets whether the user is touching the seek bar to change the track position. */
+    public void setIsScrubbing(boolean isScrubbing) {
+        if (mMediaData == null || mMediaData.getSemanticActions() == null) {
+            return;
+        }
+        if (isScrubbing == this.mIsScrubbing) {
+            return;
+        }
+        this.mIsScrubbing = isScrubbing;
+        mMainExecutor.execute(() ->
+                updateDisplayForScrubbingChange(mMediaData.getSemanticActions()));
+    }
+
     /**
      * Get the context
      *
@@ -249,6 +283,7 @@
         mSeekBarObserver = new SeekBarObserver(vh);
         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
         mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
+        mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
         mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
 
         vh.getPlayer().setOnLongClickListener(v -> {
@@ -307,6 +342,7 @@
             return;
         }
         mKey = key;
+        mMediaData = data;
         MediaSession.Token token = data.getToken();
         mPackageName = data.getPackageName();
         mUid = data.getAppUid();
@@ -361,6 +397,7 @@
         bindOutputSwitcherChip(data);
         bindLongPressMenu(data);
         bindActionButtons(data);
+        bindScrubbingTime(data);
         bindArtworkAndColors(data);
 
         // TODO: We don't need to refresh this state constantly, only if the state actually changed
@@ -544,6 +581,8 @@
         seekbar.getThumb().setTintList(textColorList);
         seekbar.setProgressTintList(textColorList);
         seekbar.setProgressBackgroundTintList(ColorStateList.valueOf(textTertiary));
+        mMediaViewHolder.getScrubbingElapsedTimeView().setTextColor(textColorList);
+        mMediaViewHolder.getScrubbingTotalTimeView().setTextColor(textColorList);
 
         // Action buttons
         mMediaViewHolder.getActionPlayPause().setBackgroundTintList(accentColorList);
@@ -589,10 +628,9 @@
             }
 
             for (int id : SEMANTIC_ACTIONS_ALL) {
-                boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(id);
                 ImageButton button = mMediaViewHolder.getAction(id);
                 MediaAction action = semanticActions.getActionById(id);
-                setSemanticButton(button, action, collapsedSet, expandedSet, showInCompact);
+                setSemanticButton(button, action);
             }
         } else {
             // Hide buttons that only appear for semantic actions
@@ -607,12 +645,21 @@
             int i = 0;
             for (; i < actions.size(); i++) {
                 boolean showInCompact = actionsWhenCollapsed.contains(i);
-                setSemanticButton(genericButtons[i], actions.get(i),  collapsedSet,
-                        expandedSet, showInCompact);
+                setGenericButton(
+                        genericButtons[i],
+                        actions.get(i),
+                        collapsedSet,
+                        expandedSet,
+                        showInCompact);
             }
             for (; i < 5; i++) {
                 // Hide any unused buttons
-                setSemanticButton(genericButtons[i], null,  collapsedSet, expandedSet, false);
+                setGenericButton(
+                        genericButtons[i],
+                        /* mediaAction= */ null,
+                        collapsedSet,
+                        expandedSet,
+                        /* showInCompact= */ false);
             }
         }
         expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
@@ -640,8 +687,19 @@
         return false;
     }
 
-    private void setSemanticButton(final ImageButton button, MediaAction mediaAction,
-            ConstraintSet collapsedSet, ConstraintSet expandedSet, boolean showInCompact) {
+    private void setGenericButton(
+            final ImageButton button,
+            @Nullable MediaAction mediaAction,
+            ConstraintSet collapsedSet,
+            ConstraintSet expandedSet,
+            boolean showInCompact) {
+        bindButtonCommon(button, mediaAction);
+        boolean visible = mediaAction != null;
+        setVisibleAndAlpha(expandedSet, button.getId(), visible);
+        setVisibleAndAlpha(collapsedSet, button.getId(), visible && showInCompact);
+    }
+
+    private void setSemanticButton(final ImageButton button, @Nullable MediaAction mediaAction) {
         AnimationBindHandler animHandler;
         if (button.getTag() == null) {
             animHandler = new AnimationBindHandler();
@@ -651,59 +709,105 @@
         }
 
         animHandler.tryExecute(() -> {
-            bindSemanticButton(animHandler, button, mediaAction,
-                               collapsedSet, expandedSet, showInCompact);
+            bindButtonWithAnimations(button, mediaAction, animHandler);
+            setSemanticButtonVisibleAndAlpha(button.getId(), mediaAction);
         });
     }
 
-    private void bindSemanticButton(final AnimationBindHandler animHandler,
-            final ImageButton button, MediaAction mediaAction, ConstraintSet collapsedSet,
-            ConstraintSet expandedSet, boolean showInCompact) {
-
+    private void bindButtonWithAnimations(
+            final ImageButton button,
+            @Nullable MediaAction mediaAction,
+            @NonNull AnimationBindHandler animHandler) {
         if (mediaAction != null) {
             if (animHandler.updateRebindId(mediaAction.getRebindId())) {
                 animHandler.unregisterAll();
-
-                final Drawable icon = mediaAction.getIcon();
-                button.setImageDrawable(icon);
-                button.setContentDescription(mediaAction.getContentDescription());
-                final Drawable bgDrawable = mediaAction.getBackground();
-                button.setBackground(bgDrawable);
-
-                animHandler.tryRegister(icon);
-                animHandler.tryRegister(bgDrawable);
-
-                Runnable action = mediaAction.getAction();
-                if (action == null) {
-                    button.setEnabled(false);
-                } else {
-                    button.setEnabled(true);
-                    button.setOnClickListener(v -> {
-                        if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                            mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
-                            logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
-                            action.run();
-
-                            if (icon instanceof Animatable) {
-                                ((Animatable) icon).start();
-                            }
-                            if (bgDrawable instanceof Animatable) {
-                                ((Animatable) bgDrawable).start();
-                            }
-                        }
-                    });
-                }
+                animHandler.tryRegister(mediaAction.getIcon());
+                animHandler.tryRegister(mediaAction.getBackground());
+                bindButtonCommon(button, mediaAction);
             }
         } else {
             animHandler.unregisterAll();
-            button.setImageDrawable(null);
-            button.setContentDescription(null);
-            button.setEnabled(false);
-            button.setBackground(null);
+            clearButton(button);
         }
+    }
 
-        setVisibleAndAlpha(collapsedSet, button.getId(), mediaAction != null && showInCompact);
-        setVisibleAndAlpha(expandedSet, button.getId(), mediaAction != null);
+    private void bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction) {
+        if (mediaAction != null) {
+            final Drawable icon = mediaAction.getIcon();
+            button.setImageDrawable(icon);
+            button.setContentDescription(mediaAction.getContentDescription());
+            final Drawable bgDrawable = mediaAction.getBackground();
+            button.setBackground(bgDrawable);
+
+            Runnable action = mediaAction.getAction();
+            if (action == null) {
+                button.setEnabled(false);
+            } else {
+                button.setEnabled(true);
+                button.setOnClickListener(v -> {
+                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
+                        logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
+                        action.run();
+
+                        if (icon instanceof Animatable) {
+                            ((Animatable) icon).start();
+                        }
+                        if (bgDrawable instanceof Animatable) {
+                            ((Animatable) bgDrawable).start();
+                        }
+                    }
+                });
+            }
+        } else {
+            clearButton(button);
+        }
+    }
+
+    private void clearButton(final ImageButton button) {
+        button.setImageDrawable(null);
+        button.setContentDescription(null);
+        button.setEnabled(false);
+        button.setBackground(null);
+    }
+
+    private void setSemanticButtonVisibleAndAlpha(
+            int buttonId,
+            MediaAction mediaAction) {
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(buttonId);
+        boolean hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId);
+        boolean shouldBeHiddenDueToScrubbing = hideWhenScrubbing && mIsScrubbing;
+        boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing;
+
+        setVisibleAndAlpha(expandedSet, buttonId, visible);
+        setVisibleAndAlpha(collapsedSet, buttonId, visible && showInCompact);
+    }
+
+    /** 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);
+        SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach((id) ->
+                setSemanticButtonVisibleAndAlpha(id, semanticActions.getActionById(id)));
+        // Trigger a state refresh so that we immediately update visibilities.
+        mMediaViewController.refreshState();
+    }
+
+    private void bindScrubbingTime(MediaData data) {
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
+        int elapsedTimeId = mMediaViewHolder.getScrubbingElapsedTimeView().getId();
+        int totalTimeId = mMediaViewHolder.getScrubbingTotalTimeView().getId();
+
+        boolean visible = data.getSemanticActions() != null && mIsScrubbing;
+        setVisibleAndAlpha(expandedSet, elapsedTimeId, visible);
+        setVisibleAndAlpha(expandedSet, totalTimeId, visible);
+        // Never show in collapsed
+        setVisibleAndAlpha(collapsedSet, elapsedTimeId, false);
+        setVisibleAndAlpha(collapsedSet, totalTimeId, false);
     }
 
     // AnimationBindHandler is responsible for tracking the bound animation state and preventing
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
index e9c8886..34a77f2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
@@ -50,8 +50,11 @@
 
     // Seekbar views
     val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
-    open val elapsedTimeView: TextView? = null
-    open val totalTimeView: TextView? = null
+    // These views are only shown while the user is actively scrubbing
+    val scrubbingElapsedTimeView: TextView =
+        itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
+    val scrubbingTotalTimeView: TextView =
+        itemView.requireViewById(R.id.media_scrubbing_total_time)
 
     // Settings screen
     val longPressText = itemView.requireViewById<TextView>(R.id.remove_text)
@@ -165,7 +168,9 @@
                 R.id.action2,
                 R.id.action3,
                 R.id.action4,
-                R.id.icon
+                R.id.icon,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
         )
         val gutsIds = setOf(
                 R.id.remove_text,
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
index b76f6bb..612a7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
@@ -70,9 +70,9 @@
             progressDrawable?.animate = false
             holder.seekBar.thumb.alpha = 0
             holder.seekBar.progress = 0
-            holder.elapsedTimeView?.text = ""
-            holder.totalTimeView?.text = ""
             holder.seekBar.contentDescription = ""
+            holder.scrubbingElapsedTimeView.text = ""
+            holder.scrubbingTotalTimeView.text = ""
             return
         }
 
@@ -88,13 +88,13 @@
         holder.seekBar.setMax(data.duration)
         val totalTimeString = DateUtils.formatElapsedTime(
             data.duration / DateUtils.SECOND_IN_MILLIS)
-        holder.totalTimeView?.setText(totalTimeString)
+        holder.scrubbingTotalTimeView.text = totalTimeString
 
         data.elapsedTime?.let {
             holder.seekBar.setProgress(it)
             val elapsedTimeString = DateUtils.formatElapsedTime(
                 it / DateUtils.SECOND_IN_MILLIS)
-            holder.elapsedTimeView?.setText(elapsedTimeString)
+            holder.scrubbingElapsedTimeView.text = elapsedTimeString
 
             holder.seekBar.contentDescription = holder.seekBar.context.getString(
                 R.string.controls_media_seekbar_description,
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
index 8c1845a..5218492 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
@@ -121,12 +121,15 @@
             }
         }
 
+    private var scrubbingChangeListener: ScrubbingChangeListener? = null
+
     /** Set to true when the user is touching the seek bar to change the position. */
     private var scrubbing = false
         set(value) {
             if (field != value) {
                 field = value
                 checkIfPollingNeeded()
+                scrubbingChangeListener?.onScrubbingChanged(value)
                 _data = _data.copy(scrubbing = value)
             }
         }
@@ -228,6 +231,7 @@
         playbackState = null
         cancel?.run()
         cancel = null
+        scrubbingChangeListener = null
     }
 
     @WorkerThread
@@ -265,6 +269,21 @@
         bar.setOnTouchListener(SeekBarTouchListener(this, bar))
     }
 
+    fun setScrubbingChangeListener(listener: ScrubbingChangeListener) {
+        scrubbingChangeListener = listener
+    }
+
+    fun removeScrubbingChangeListener(listener: ScrubbingChangeListener) {
+        if (listener == scrubbingChangeListener) {
+            scrubbingChangeListener = null
+        }
+    }
+
+    /** Listener interface to be notified when the user starts or stops scrubbing. */
+    interface ScrubbingChangeListener {
+        fun onScrubbingChanged(scrubbing: 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 538a9c7..dc48eb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.KotlinArgumentCaptor
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import dagger.Lazy
@@ -68,6 +69,7 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
@@ -91,6 +93,7 @@
     private lateinit var player: MediaControlPanel
 
     private lateinit var bgExecutor: FakeExecutor
+    private lateinit var mainExecutor: FakeExecutor
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var broadcastSender: BroadcastSender
 
@@ -116,8 +119,6 @@
     private lateinit var seamlessIcon: ImageView
     private lateinit var seamlessText: TextView
     private lateinit var seekBar: SeekBar
-    private lateinit var elapsedTimeView: TextView
-    private lateinit var totalTimeView: TextView
     private lateinit var action0: ImageButton
     private lateinit var action1: ImageButton
     private lateinit var action2: ImageButton
@@ -126,6 +127,8 @@
     private lateinit var actionPlayPause: ImageButton
     private lateinit var actionNext: ImageButton
     private lateinit var actionPrev: ImageButton
+    private lateinit var scrubbingElapsedTimeView: TextView
+    private lateinit var scrubbingTotalTimeView: TextView
     private lateinit var actionsTopBarrier: Barrier
     @Mock private lateinit var longPressText: TextView
     @Mock private lateinit var handler: Handler
@@ -148,12 +151,25 @@
     @Before
     fun setUp() {
         bgExecutor = FakeExecutor(FakeSystemClock())
+        mainExecutor = FakeExecutor(FakeSystemClock())
         whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
         whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
 
-        player = MediaControlPanel(context, bgExecutor, activityStarter, broadcastSender,
-            mediaViewController, seekBarViewModel, Lazy { mediaDataManager },
-            mediaOutputDialogFactory, mediaCarouselController, falsingManager, clock, logger)
+        player = MediaControlPanel(
+            context,
+            bgExecutor,
+            mainExecutor,
+            activityStarter,
+            broadcastSender,
+            mediaViewController,
+            seekBarViewModel,
+            Lazy { mediaDataManager },
+            mediaOutputDialogFactory,
+            mediaCarouselController,
+            falsingManager,
+            clock,
+            logger
+        )
         whenever(seekBarViewModel.progress).thenReturn(seekBarData)
 
         // Set up mock views for the players
@@ -167,8 +183,6 @@
         seamlessIcon = ImageView(context)
         seamlessText = TextView(context)
         seekBar = SeekBar(context)
-        elapsedTimeView = TextView(context)
-        totalTimeView = TextView(context)
         settings = ImageButton(context)
         cancel = View(context)
         cancelText = TextView(context)
@@ -184,6 +198,10 @@
         actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) }
         actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) }
         actionNext = ImageButton(context).also { it.setId(R.id.actionNext) }
+        scrubbingElapsedTimeView =
+            TextView(context).also { it.setId(R.id.media_scrubbing_elapsed_time) }
+        scrubbingTotalTimeView =
+            TextView(context).also { it.setId(R.id.media_scrubbing_total_time) }
 
         actionsTopBarrier =
             Barrier(context).also {
@@ -242,6 +260,8 @@
         whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon)
         whenever(viewHolder.seamlessText).thenReturn(seamlessText)
         whenever(viewHolder.seekBar).thenReturn(seekBar)
+        whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
+        whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
 
         // Transition View
         whenever(view.parent).thenReturn(transitionParent)
@@ -366,6 +386,86 @@
     }
 
     @Test
+    fun bind_notScrubbing_scrubbingViewsGone() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions = MediaButton(
+            prevOrCustom = MediaAction(icon, {}, "prev", null),
+            nextOrCustom = MediaAction(icon, {}, "next", null),
+        )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_noSemanticActions_viewsNotChanged() {
+        val state = mediaData.copy(semanticActions = null)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        val listener = getScrubbingChangeListener()
+
+        listener.onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_elapsed_time), anyInt())
+        verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_total_time), anyInt())
+    }
+
+    @Test
+    fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions = MediaButton(
+            prevOrCustom = MediaAction(icon, {}, "prev", null),
+            nextOrCustom = MediaAction(icon, {}, "next", null),
+        )
+        val state = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        // Only in expanded, we should show the scrubbing times and hide prev+next
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions = MediaButton(
+            prevOrCustom = MediaAction(icon, {}, "prev", null),
+            nextOrCustom = MediaAction(icon, {}, "next", null),
+        )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(false)
+        mainExecutor.runAllReady()
+
+        // Only in expanded, we should hide the scrubbing times and show prev+next
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
+    }
+
+    @Test
     fun bindNotificationActions() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
@@ -780,4 +880,7 @@
 
         verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
     }
+
+    private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
+        withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
index e719e84..c48d846 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
@@ -46,8 +46,8 @@
     @Mock private lateinit var mockHolder: MediaViewHolder
     @Mock private lateinit var mockSquigglyProgress: SquigglyProgress
     private lateinit var seekBarView: SeekBar
-    private lateinit var elapsedTimeView: TextView
-    private lateinit var totalTimeView: TextView
+    private lateinit var scrubbingElapsedTimeView: TextView
+    private lateinit var scrubbingTotalTimeView: TextView
 
     @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
 
@@ -60,9 +60,11 @@
 
         seekBarView = SeekBar(context)
         seekBarView.progressDrawable = mockSquigglyProgress
-        elapsedTimeView = TextView(context)
-        totalTimeView = TextView(context)
+        scrubbingElapsedTimeView = TextView(context)
+        scrubbingTotalTimeView = TextView(context)
         whenever(mockHolder.seekBar).thenReturn(seekBarView)
+        whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
+        whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
 
         observer = SeekBarObserver(mockHolder)
     }
@@ -167,4 +169,24 @@
         // THEN progress drawable is not animating
         verify(mockSquigglyProgress).animate = false
     }
+
+    @Test
+    fun seekBarProgress_enabled_timeViewsHaveTime() {
+        val data = SeekBarViewModel.Progress(enabled = true, true, true, false, 3000, 120000)
+
+        observer.onChanged(data)
+
+        assertThat(scrubbingElapsedTimeView.text).isEqualTo("00:03")
+        assertThat(scrubbingTotalTimeView.text).isEqualTo("02:00")
+    }
+
+    @Test
+    fun seekBarProgress_disabled_timeViewsEmpty() {
+        val data = SeekBarViewModel.Progress(enabled = false, true, true, false, 3000, 120000)
+
+        observer.onChanged(data)
+
+        assertThat(scrubbingElapsedTimeView.text).isEqualTo("")
+        assertThat(scrubbingTotalTimeView.text).isEqualTo("")
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
index 20f5e4c..afc9c81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
@@ -324,6 +324,42 @@
     }
 
     @Test
+    fun seekStarted_listenerNotified() {
+        var isScrubbing: Boolean? = null
+        val listener = object : SeekBarViewModel.ScrubbingChangeListener {
+            override fun onScrubbingChanged(scrubbing: Boolean) {
+                isScrubbing = scrubbing
+            }
+        }
+        viewModel.setScrubbingChangeListener(listener)
+
+        viewModel.onSeekStarting()
+        fakeExecutor.runAllReady()
+
+        assertThat(isScrubbing).isTrue()
+    }
+
+    @Test
+    fun seekEnded_listenerNotified() {
+        var isScrubbing: Boolean? = null
+        val listener = object : SeekBarViewModel.ScrubbingChangeListener {
+            override fun onScrubbingChanged(scrubbing: Boolean) {
+                isScrubbing = scrubbing
+            }
+        }
+        viewModel.setScrubbingChangeListener(listener)
+
+        // Start seeking
+        viewModel.onSeekStarting()
+        fakeExecutor.runAllReady()
+        // End seeking
+        viewModel.onSeek(15L)
+        fakeExecutor.runAllReady()
+
+        assertThat(isScrubbing).isFalse()
+    }
+
+    @Test
     @Ignore
     fun onProgressChangedFromUser() {
         // WHEN user starts dragging the seek bar