[automerge] [Media UMO] Show times when scrubbing. 2p: f9942f0ad8

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17475783

Bug: 209656742
Change-Id: I13b37abd7fee5ac7fd4f2cde823833017979055d
This commit is contained in:
Presubmit Automerger Backend
2022-04-07 01:11:21 +00:00
11 changed files with 429 additions and 75 deletions

View File

@@ -157,7 +157,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:barrierDirection="start" 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 <androidx.constraintlayout.widget.Barrier
android:id="@+id/media_action_barrier_end" android:id="@+id/media_action_barrier_end"
@@ -167,7 +167,7 @@
app:layout_constraintTop_toBottomOf="@id/media_seamless" app:layout_constraintTop_toBottomOf="@id/media_seamless"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:barrierDirection="end" 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" app:layout_constraintStart_toStartOf="parent"
/> />
@@ -177,7 +177,7 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:barrierDirection="top" 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 --> <!-- Button visibility will be controlled in code -->
@@ -192,6 +192,22 @@
android:layout_marginTop="0dp" 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 --> <!-- Seek Bar -->
<!-- As per Material Design on Bidirectionality, this is forced to LTR in code --> <!-- As per Material Design on Bidirectionality, this is forced to LTR in code -->
<SeekBar <SeekBar
@@ -218,6 +234,22 @@
android:layout_marginBottom="@dimen/qs_media_padding" android:layout_marginBottom="@dimen/qs_media_padding"
android:layout_marginTop="0dp" /> 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 <ImageButton
android:id="@+id/action0" android:id="@+id/action0"
style="@style/MediaPlayer.SessionAction.Secondary" style="@style/MediaPlayer.SessionAction.Secondary"

View File

@@ -601,6 +601,12 @@
<item name="android:textColor">?android:attr/textColorSecondary</item> <item name="android:textColor">?android:attr/textColorSecondary</item>
</style> </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"> <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:background">@drawable/qs_media_light_source</item>
<item name="android:tint">?android:attr/textColorPrimary</item> <item name="android:tint">?android:attr/textColorPrimary</item>

View File

@@ -93,6 +93,11 @@
app:layout_constraintTop_toBottomOf="@id/media_seamless" app:layout_constraintTop_toBottomOf="@id/media_seamless"
app:layout_constraintLeft_toRightOf="@id/media_action_barrier" /> 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 <Constraint
android:id="@+id/media_progress_bar" android:id="@+id/media_progress_bar"
android:layout_width="0dp" android:layout_width="0dp"
@@ -116,6 +121,11 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/media_seamless" /> 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 <Constraint
android:id="@+id/action0" android:id="@+id/action0"
android:layout_width="48dp" 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. The chain is set to "spread" so that the progress bar can be weighted to fill any empty space.
--> -->
<Constraint <Constraint
android:id="@+id/actionPrev" android:id="@+id/media_scrubbing_elapsed_time"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
app:layout_constraintLeft_toLeftOf="parent" 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_constraintRight_toLeftOf="@id/media_progress_bar"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="spread" /> app:layout_constraintHorizontal_chainStyle="spread" />
@@ -90,6 +99,14 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
app:layout_constraintLeft_toRightOf="@id/media_progress_bar" 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_constraintRight_toLeftOf="@id/action0"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
@@ -97,7 +114,7 @@
android:id="@+id/action0" android:id="@+id/action0"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="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_constraintRight_toLeftOf="@id/action1"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
@@ -115,7 +132,7 @@
android:layout_height="48dp" android:layout_height="48dp"
app:layout_constraintLeft_toRightOf="@id/action1" app:layout_constraintLeft_toRightOf="@id/action1"
app:layout_constraintRight_toLeftOf="@id/action3" app:layout_constraintRight_toLeftOf="@id/action3"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent" />
<Constraint <Constraint
android:id="@+id/action3" android:id="@+id/action3"
@@ -123,7 +140,7 @@
android:layout_height="48dp" android:layout_height="48dp"
app:layout_constraintLeft_toRightOf="@id/action2" app:layout_constraintLeft_toRightOf="@id/action2"
app:layout_constraintRight_toLeftOf="@id/action4" app:layout_constraintRight_toLeftOf="@id/action4"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent" />
<Constraint <Constraint
android:id="@+id/action4" 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.animation.GhostedViewLaunchAnimatorController;
import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.qualifiers.Background; 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.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.monet.ColorScheme; import com.android.systemui.monet.ColorScheme;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
@@ -108,6 +109,13 @@ public class MediaControlPanel {
R.id.actionNext 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 // Buttons to show in small player when using semantic actions
private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of( private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of(
R.id.actionPlayPause, R.id.actionPlayPause,
@@ -120,6 +128,7 @@ public class MediaControlPanel {
private final SeekBarViewModel mSeekBarViewModel; private final SeekBarViewModel mSeekBarViewModel;
private SeekBarObserver mSeekBarObserver; private SeekBarObserver mSeekBarObserver;
protected final Executor mBackgroundExecutor; protected final Executor mBackgroundExecutor;
private final Executor mMainExecutor;
private final ActivityStarter mActivityStarter; private final ActivityStarter mActivityStarter;
private final BroadcastSender mBroadcastSender; private final BroadcastSender mBroadcastSender;
@@ -127,6 +136,7 @@ public class MediaControlPanel {
private MediaViewHolder mMediaViewHolder; private MediaViewHolder mMediaViewHolder;
private RecommendationViewHolder mRecommendationViewHolder; private RecommendationViewHolder mRecommendationViewHolder;
private String mKey; private String mKey;
private MediaData mMediaData;
private MediaViewController mMediaViewController; private MediaViewController mMediaViewController;
private MediaSession.Token mToken; private MediaSession.Token mToken;
private MediaController mController; private MediaController mController;
@@ -147,14 +157,23 @@ public class MediaControlPanel {
protected int mSmartspaceId = -1; protected int mSmartspaceId = -1;
private String mPackageName; private String mPackageName;
private boolean mIsScrubbing = false;
private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
this::setIsScrubbing;
/** /**
* Initialize a new control panel * Initialize a new control panel
* *
* @param backgroundExecutor background executor, used for processing artwork * @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 * @param activityStarter activity starter
*/ */
@Inject @Inject
public MediaControlPanel(Context context, @Background Executor backgroundExecutor, public MediaControlPanel(Context context,
@Background Executor backgroundExecutor,
@Main Executor mainExecutor,
ActivityStarter activityStarter, BroadcastSender broadcastSender, ActivityStarter activityStarter, BroadcastSender broadcastSender,
MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel,
Lazy<MediaDataManager> lazyMediaDataManager, Lazy<MediaDataManager> lazyMediaDataManager,
@@ -163,6 +182,7 @@ public class MediaControlPanel {
FalsingManager falsingManager, SystemClock systemClock, MediaUiEventLogger logger) { FalsingManager falsingManager, SystemClock systemClock, MediaUiEventLogger logger) {
mContext = context; mContext = context;
mBackgroundExecutor = backgroundExecutor; mBackgroundExecutor = backgroundExecutor;
mMainExecutor = mainExecutor;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
mBroadcastSender = broadcastSender; mBroadcastSender = broadcastSender;
mSeekBarViewModel = seekBarViewModel; mSeekBarViewModel = seekBarViewModel;
@@ -186,6 +206,7 @@ public class MediaControlPanel {
public void onDestroy() { public void onDestroy() {
if (mSeekBarObserver != null) { if (mSeekBarObserver != null) {
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
} }
mSeekBarViewModel.onDestroy(); mSeekBarViewModel.onDestroy();
mMediaViewController.onDestroy(); mMediaViewController.onDestroy();
@@ -232,6 +253,19 @@ public class MediaControlPanel {
mSeekBarViewModel.setListening(listening); 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 * Get the context
* *
@@ -249,6 +283,7 @@ public class MediaControlPanel {
mSeekBarObserver = new SeekBarObserver(vh); mSeekBarObserver = new SeekBarObserver(vh);
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER); mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
vh.getPlayer().setOnLongClickListener(v -> { vh.getPlayer().setOnLongClickListener(v -> {
@@ -307,6 +342,7 @@ public class MediaControlPanel {
return; return;
} }
mKey = key; mKey = key;
mMediaData = data;
MediaSession.Token token = data.getToken(); MediaSession.Token token = data.getToken();
mPackageName = data.getPackageName(); mPackageName = data.getPackageName();
mUid = data.getAppUid(); mUid = data.getAppUid();
@@ -361,6 +397,7 @@ public class MediaControlPanel {
bindOutputSwitcherChip(data); bindOutputSwitcherChip(data);
bindLongPressMenu(data); bindLongPressMenu(data);
bindActionButtons(data); bindActionButtons(data);
bindScrubbingTime(data);
bindArtworkAndColors(data); bindArtworkAndColors(data);
// TODO: We don't need to refresh this state constantly, only if the state actually changed // 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.getThumb().setTintList(textColorList);
seekbar.setProgressTintList(textColorList); seekbar.setProgressTintList(textColorList);
seekbar.setProgressBackgroundTintList(ColorStateList.valueOf(textTertiary)); seekbar.setProgressBackgroundTintList(ColorStateList.valueOf(textTertiary));
mMediaViewHolder.getScrubbingElapsedTimeView().setTextColor(textColorList);
mMediaViewHolder.getScrubbingTotalTimeView().setTextColor(textColorList);
// Action buttons // Action buttons
mMediaViewHolder.getActionPlayPause().setBackgroundTintList(accentColorList); mMediaViewHolder.getActionPlayPause().setBackgroundTintList(accentColorList);
@@ -589,10 +628,9 @@ public class MediaControlPanel {
} }
for (int id : SEMANTIC_ACTIONS_ALL) { for (int id : SEMANTIC_ACTIONS_ALL) {
boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(id);
ImageButton button = mMediaViewHolder.getAction(id); ImageButton button = mMediaViewHolder.getAction(id);
MediaAction action = semanticActions.getActionById(id); MediaAction action = semanticActions.getActionById(id);
setSemanticButton(button, action, collapsedSet, expandedSet, showInCompact); setSemanticButton(button, action);
} }
} else { } else {
// Hide buttons that only appear for semantic actions // Hide buttons that only appear for semantic actions
@@ -607,12 +645,21 @@ public class MediaControlPanel {
int i = 0; int i = 0;
for (; i < actions.size(); i++) { for (; i < actions.size(); i++) {
boolean showInCompact = actionsWhenCollapsed.contains(i); boolean showInCompact = actionsWhenCollapsed.contains(i);
setSemanticButton(genericButtons[i], actions.get(i), collapsedSet, setGenericButton(
expandedSet, showInCompact); genericButtons[i],
actions.get(i),
collapsedSet,
expandedSet,
showInCompact);
} }
for (; i < 5; i++) { for (; i < 5; i++) {
// Hide any unused buttons // 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()); expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
@@ -640,8 +687,19 @@ public class MediaControlPanel {
return false; return false;
} }
private void setSemanticButton(final ImageButton button, MediaAction mediaAction, private void setGenericButton(
ConstraintSet collapsedSet, ConstraintSet expandedSet, boolean showInCompact) { 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; AnimationBindHandler animHandler;
if (button.getTag() == null) { if (button.getTag() == null) {
animHandler = new AnimationBindHandler(); animHandler = new AnimationBindHandler();
@@ -651,59 +709,105 @@ public class MediaControlPanel {
} }
animHandler.tryExecute(() -> { animHandler.tryExecute(() -> {
bindSemanticButton(animHandler, button, mediaAction, bindButtonWithAnimations(button, mediaAction, animHandler);
collapsedSet, expandedSet, showInCompact); setSemanticButtonVisibleAndAlpha(button.getId(), mediaAction);
}); });
} }
private void bindSemanticButton(final AnimationBindHandler animHandler, private void bindButtonWithAnimations(
final ImageButton button, MediaAction mediaAction, ConstraintSet collapsedSet, final ImageButton button,
ConstraintSet expandedSet, boolean showInCompact) { @Nullable MediaAction mediaAction,
@NonNull AnimationBindHandler animHandler) {
if (mediaAction != null) { if (mediaAction != null) {
if (animHandler.updateRebindId(mediaAction.getRebindId())) { if (animHandler.updateRebindId(mediaAction.getRebindId())) {
animHandler.unregisterAll(); animHandler.unregisterAll();
animHandler.tryRegister(mediaAction.getIcon());
final Drawable icon = mediaAction.getIcon(); animHandler.tryRegister(mediaAction.getBackground());
button.setImageDrawable(icon); bindButtonCommon(button, mediaAction);
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();
}
}
});
}
} }
} else { } else {
animHandler.unregisterAll(); animHandler.unregisterAll();
button.setImageDrawable(null); clearButton(button);
button.setContentDescription(null);
button.setEnabled(false);
button.setBackground(null);
} }
}
setVisibleAndAlpha(collapsedSet, button.getId(), mediaAction != null && showInCompact); private void bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction) {
setVisibleAndAlpha(expandedSet, button.getId(), mediaAction != null); 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 // AnimationBindHandler is responsible for tracking the bound animation state and preventing

View File

@@ -50,8 +50,11 @@ class MediaViewHolder constructor(itemView: View) {
// Seekbar views // Seekbar views
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar) val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
open val elapsedTimeView: TextView? = null // These views are only shown while the user is actively scrubbing
open val totalTimeView: TextView? = null val scrubbingElapsedTimeView: TextView =
itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
val scrubbingTotalTimeView: TextView =
itemView.requireViewById(R.id.media_scrubbing_total_time)
// Settings screen // Settings screen
val longPressText = itemView.requireViewById<TextView>(R.id.remove_text) val longPressText = itemView.requireViewById<TextView>(R.id.remove_text)
@@ -165,7 +168,9 @@ class MediaViewHolder constructor(itemView: View) {
R.id.action2, R.id.action2,
R.id.action3, R.id.action3,
R.id.action4, R.id.action4,
R.id.icon R.id.icon,
R.id.media_scrubbing_elapsed_time,
R.id.media_scrubbing_total_time
) )
val gutsIds = setOf( val gutsIds = setOf(
R.id.remove_text, R.id.remove_text,

View File

@@ -70,9 +70,9 @@ class SeekBarObserver(
progressDrawable?.animate = false progressDrawable?.animate = false
holder.seekBar.thumb.alpha = 0 holder.seekBar.thumb.alpha = 0
holder.seekBar.progress = 0 holder.seekBar.progress = 0
holder.elapsedTimeView?.text = ""
holder.totalTimeView?.text = ""
holder.seekBar.contentDescription = "" holder.seekBar.contentDescription = ""
holder.scrubbingElapsedTimeView.text = ""
holder.scrubbingTotalTimeView.text = ""
return return
} }
@@ -88,13 +88,13 @@ class SeekBarObserver(
holder.seekBar.setMax(data.duration) holder.seekBar.setMax(data.duration)
val totalTimeString = DateUtils.formatElapsedTime( val totalTimeString = DateUtils.formatElapsedTime(
data.duration / DateUtils.SECOND_IN_MILLIS) data.duration / DateUtils.SECOND_IN_MILLIS)
holder.totalTimeView?.setText(totalTimeString) holder.scrubbingTotalTimeView.text = totalTimeString
data.elapsedTime?.let { data.elapsedTime?.let {
holder.seekBar.setProgress(it) holder.seekBar.setProgress(it)
val elapsedTimeString = DateUtils.formatElapsedTime( val elapsedTimeString = DateUtils.formatElapsedTime(
it / DateUtils.SECOND_IN_MILLIS) it / DateUtils.SECOND_IN_MILLIS)
holder.elapsedTimeView?.setText(elapsedTimeString) holder.scrubbingElapsedTimeView.text = elapsedTimeString
holder.seekBar.contentDescription = holder.seekBar.context.getString( holder.seekBar.contentDescription = holder.seekBar.context.getString(
R.string.controls_media_seekbar_description, 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. */ /** Set to true when the user is touching the seek bar to change the position. */
private var scrubbing = false private var scrubbing = false
set(value) { set(value) {
if (field != value) { if (field != value) {
field = value field = value
checkIfPollingNeeded() checkIfPollingNeeded()
scrubbingChangeListener?.onScrubbingChanged(value)
_data = _data.copy(scrubbing = value) _data = _data.copy(scrubbing = value)
} }
} }
@@ -228,6 +231,7 @@ class SeekBarViewModel @Inject constructor(
playbackState = null playbackState = null
cancel?.run() cancel?.run()
cancel = null cancel = null
scrubbingChangeListener = null
} }
@WorkerThread @WorkerThread
@@ -265,6 +269,21 @@ class SeekBarViewModel @Inject constructor(
bar.setOnTouchListener(SeekBarTouchListener(this, bar)) 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( private class SeekBarChangeListener(
val viewModel: SeekBarViewModel val viewModel: SeekBarViewModel
) : SeekBar.OnSeekBarChangeListener { ) : 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.concurrency.FakeExecutor
import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.KotlinArgumentCaptor
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.Lazy import dagger.Lazy
@@ -68,6 +69,7 @@ import org.mockito.Mock
import org.mockito.Mockito.any import org.mockito.Mockito.any
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.times import org.mockito.Mockito.times
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
@@ -91,6 +93,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var player: MediaControlPanel private lateinit var player: MediaControlPanel
private lateinit var bgExecutor: FakeExecutor private lateinit var bgExecutor: FakeExecutor
private lateinit var mainExecutor: FakeExecutor
@Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var broadcastSender: BroadcastSender @Mock private lateinit var broadcastSender: BroadcastSender
@@ -116,8 +119,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var seamlessIcon: ImageView private lateinit var seamlessIcon: ImageView
private lateinit var seamlessText: TextView private lateinit var seamlessText: TextView
private lateinit var seekBar: SeekBar private lateinit var seekBar: SeekBar
private lateinit var elapsedTimeView: TextView
private lateinit var totalTimeView: TextView
private lateinit var action0: ImageButton private lateinit var action0: ImageButton
private lateinit var action1: ImageButton private lateinit var action1: ImageButton
private lateinit var action2: ImageButton private lateinit var action2: ImageButton
@@ -126,6 +127,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var actionPlayPause: ImageButton private lateinit var actionPlayPause: ImageButton
private lateinit var actionNext: ImageButton private lateinit var actionNext: ImageButton
private lateinit var actionPrev: ImageButton private lateinit var actionPrev: ImageButton
private lateinit var scrubbingElapsedTimeView: TextView
private lateinit var scrubbingTotalTimeView: TextView
private lateinit var actionsTopBarrier: Barrier private lateinit var actionsTopBarrier: Barrier
@Mock private lateinit var longPressText: TextView @Mock private lateinit var longPressText: TextView
@Mock private lateinit var handler: Handler @Mock private lateinit var handler: Handler
@@ -148,12 +151,25 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
bgExecutor = FakeExecutor(FakeSystemClock()) bgExecutor = FakeExecutor(FakeSystemClock())
mainExecutor = FakeExecutor(FakeSystemClock())
whenever(mediaViewController.expandedLayout).thenReturn(expandedSet) whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet) whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
player = MediaControlPanel(context, bgExecutor, activityStarter, broadcastSender, player = MediaControlPanel(
mediaViewController, seekBarViewModel, Lazy { mediaDataManager }, context,
mediaOutputDialogFactory, mediaCarouselController, falsingManager, clock, logger) bgExecutor,
mainExecutor,
activityStarter,
broadcastSender,
mediaViewController,
seekBarViewModel,
Lazy { mediaDataManager },
mediaOutputDialogFactory,
mediaCarouselController,
falsingManager,
clock,
logger
)
whenever(seekBarViewModel.progress).thenReturn(seekBarData) whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Set up mock views for the players // Set up mock views for the players
@@ -167,8 +183,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
seamlessIcon = ImageView(context) seamlessIcon = ImageView(context)
seamlessText = TextView(context) seamlessText = TextView(context)
seekBar = SeekBar(context) seekBar = SeekBar(context)
elapsedTimeView = TextView(context)
totalTimeView = TextView(context)
settings = ImageButton(context) settings = ImageButton(context)
cancel = View(context) cancel = View(context)
cancelText = TextView(context) cancelText = TextView(context)
@@ -184,6 +198,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) } actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) }
actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) } actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) }
actionNext = ImageButton(context).also { it.setId(R.id.actionNext) } 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 = actionsTopBarrier =
Barrier(context).also { Barrier(context).also {
@@ -242,6 +260,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon) whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon)
whenever(viewHolder.seamlessText).thenReturn(seamlessText) whenever(viewHolder.seamlessText).thenReturn(seamlessText)
whenever(viewHolder.seekBar).thenReturn(seekBar) whenever(viewHolder.seekBar).thenReturn(seekBar)
whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
// Transition View // Transition View
whenever(view.parent).thenReturn(transitionParent) whenever(view.parent).thenReturn(transitionParent)
@@ -365,6 +385,86 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(expandedSet).setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE) 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 @Test
fun bindNotificationActions() { fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) 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)) 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 mockHolder: MediaViewHolder
@Mock private lateinit var mockSquigglyProgress: SquigglyProgress @Mock private lateinit var mockSquigglyProgress: SquigglyProgress
private lateinit var seekBarView: SeekBar private lateinit var seekBarView: SeekBar
private lateinit var elapsedTimeView: TextView private lateinit var scrubbingElapsedTimeView: TextView
private lateinit var totalTimeView: TextView private lateinit var scrubbingTotalTimeView: TextView
@JvmField @Rule val mockitoRule = MockitoJUnit.rule() @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
@@ -60,9 +60,11 @@ class SeekBarObserverTest : SysuiTestCase() {
seekBarView = SeekBar(context) seekBarView = SeekBar(context)
seekBarView.progressDrawable = mockSquigglyProgress seekBarView.progressDrawable = mockSquigglyProgress
elapsedTimeView = TextView(context) scrubbingElapsedTimeView = TextView(context)
totalTimeView = TextView(context) scrubbingTotalTimeView = TextView(context)
whenever(mockHolder.seekBar).thenReturn(seekBarView) whenever(mockHolder.seekBar).thenReturn(seekBarView)
whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
observer = SeekBarObserver(mockHolder) observer = SeekBarObserver(mockHolder)
} }
@@ -167,4 +169,24 @@ class SeekBarObserverTest : SysuiTestCase() {
// THEN progress drawable is not animating // THEN progress drawable is not animating
verify(mockSquigglyProgress).animate = false 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() 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 @Test
@Ignore @Ignore
fun onProgressChangedFromUser() { fun onProgressChangedFromUser() {