[Media UMO] Show times when scrubbing.

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

Change-Id: I32687652d20715dc9d4775ec149e04e4473e9e1b
This commit is contained in:
Caitlin Cassidy
2022-03-29 18:35:29 +00:00
parent 3ed0308046
commit f9942f0ad8
11 changed files with 429 additions and 75 deletions

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -60,6 +60,7 @@ import com.android.systemui.animation.ActivityLaunchAnimator;
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
FalsingManager falsingManager, SystemClock systemClock, MediaUiEventLogger logger) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
mMainExecutor = mainExecutor;
mActivityStarter = activityStarter;
mBroadcastSender = broadcastSender;
mSeekBarViewModel = seekBarViewModel;
@@ -186,6 +206,7 @@ public class MediaControlPanel {
public void onDestroy() {
if (mSeekBarObserver != null) {
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
}
mSeekBarViewModel.onDestroy();
mMediaViewController.onDestroy();
@@ -232,6 +253,19 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
return;
}
mKey = key;
mMediaData = data;
MediaSession.Token token = data.getToken();
mPackageName = data.getPackageName();
mUid = data.getAppUid();
@@ -361,6 +397,7 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
}
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
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 @@ public class MediaControlPanel {
}
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

View File

@@ -50,8 +50,11 @@ class MediaViewHolder constructor(itemView: View) {
// 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 @@ class MediaViewHolder constructor(itemView: View) {
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,

View File

@@ -70,9 +70,9 @@ class SeekBarObserver(
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 @@ class SeekBarObserver(
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,

View File

@@ -121,12 +121,15 @@ class SeekBarViewModel @Inject constructor(
}
}
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 @@ class SeekBarViewModel @Inject constructor(
playbackState = null
cancel?.run()
cancel = null
scrubbingChangeListener = null
}
@WorkerThread
@@ -265,6 +269,21 @@ class SeekBarViewModel @Inject constructor(
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 {

View File

@@ -53,6 +53,7 @@ import com.android.systemui.util.animation.TransitionLayout
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.Mock
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
@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 @@ public class MediaControlPanelTest : SysuiTestCase() {
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
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)
@@ -365,6 +385,86 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(expandedSet).setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE)
}
@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)
@@ -780,4 +880,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
}
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
}

View File

@@ -46,8 +46,8 @@ class SeekBarObserverTest : SysuiTestCase() {
@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 @@ class SeekBarObserverTest : SysuiTestCase() {
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 @@ class SeekBarObserverTest : SysuiTestCase() {
// 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("")
}
}

View File

@@ -323,6 +323,42 @@ public class SeekBarViewModelTest : SysuiTestCase() {
assertThat(viewModel.progress.value!!.elapsedTime).isNull()
}
@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() {