[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