Merge "Add new layout for session based media players"

This commit is contained in:
Beth Thibodeau
2021-12-22 21:23:58 +00:00
committed by Android (Google) Code Review
15 changed files with 947 additions and 298 deletions

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/media_seamless_border">
<item android:id="@android:id/background">
<shape android:shape="oval">
<solid android:color="@color/media_seamless_border" />
<size android:width="48dp" android:height="48dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,313 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!-- Layout for media session-based controls -->
<com.android.systemui.util.animation.TransitionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/qs_media_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_horizontal|fill_vertical"
android:forceHasOverlappingRendering="false"
android:background="@drawable/qs_media_background"
android:theme="@style/MediaPlayer">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/center_vertical_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.6" />
<!-- App icon -->
<com.android.internal.widget.CachingIconView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="@dimen/qs_media_padding"
android:layout_marginTop="@dimen/qs_media_padding"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Seamless Output Switcher -->
<LinearLayout
android:id="@+id/media_seamless"
android:orientation="horizontal"
android:gravity="top|end"
android:paddingTop="@dimen/qs_media_padding"
android:paddingEnd="@dimen/qs_media_padding"
android:background="@drawable/qs_media_light_source"
android:forceHasOverlappingRendering="false"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="@dimen/qs_center_guideline_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/center_vertical_guideline"
app:layout_constraintHorizontal_bias="1"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
app:layout_constraintHeight_min="@dimen/min_clickable_item_size">
<LinearLayout
android:id="@+id/media_seamless_button"
android:layout_width="wrap_content"
android:layout_height="@dimen/qs_seamless_height"
android:minHeight="@dimen/qs_seamless_height"
android:theme="@style/MediaPlayer.SolidButton"
android:background="@drawable/qs_media_seamless_background"
android:orientation="horizontal"
android:contentDescription="@string/quick_settings_media_device_label">
<ImageView
android:id="@+id/media_seamless_image"
android:layout_width="@dimen/qs_seamless_icon_size"
android:layout_height="@dimen/qs_seamless_icon_size"
android:layout_gravity="center"
android:tint="?android:attr/textColorPrimary"
android:src="@*android:drawable/ic_media_seamless" />
<TextView
android:id="@+id/media_seamless_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:singleLine="true"
android:text="@*android:string/ext_media_seamless_action"
android:textDirection="locale"
android:textSize="12sp"
android:lineHeight="16sp" />
</LinearLayout>
</LinearLayout>
<!-- Song name -->
<TextView
android:id="@+id/header_title"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:singleLine="true"
android:textSize="16sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="@dimen/qs_media_padding"
android:layout_marginEnd="@dimen/qs_media_padding"
app:layout_constrainedWidth="true"
app:layout_constraintTop_toBottomOf="@id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/actionPlayPause"
app:layout_constraintHorizontal_bias="0" />
<!-- Artist name -->
<TextView
android:id="@+id/header_artist"
android:fontFamily="@*android:string/config_headlineFontFamily"
android:singleLine="true"
style="@style/MediaPlayer.Subtitle"
android:textSize="14sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/qs_media_padding"
app:layout_constrainedWidth="true"
android:layout_marginTop="1dp"
app:layout_constraintTop_toBottomOf="@id/header_title"
app:layout_constraintStart_toStartOf="@id/header_title"
app:layout_constraintEnd_toStartOf="@id/actionPlayPause"
app:layout_constraintBottom_toBottomOf="@id/actionPlayPause"
app:layout_constraintHorizontal_bias="0" />
<ImageButton
android:id="@+id/actionPlayPause"
style="@style/MediaPlayer.SessionAction.Primary"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/qs_media_padding"
android:layout_marginEnd="@dimen/qs_media_padding"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/media_seamless"
app:layout_constraintBottom_toTopOf="@id/actionEnd" />
<ImageButton
android:id="@+id/actionPrev"
style="@style/MediaPlayer.SessionAction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/media_progress_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/actionPlayPause" />
<!-- Seek Bar -->
<!-- As per Material Design on Bidirectionality, this is forced to LTR in code -->
<SeekBar
android:id="@+id/media_progress_bar"
style="@style/MediaPlayer.ProgressBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="@dimen/qs_media_session_enabled_seekbar_vertical_padding"
android:paddingBottom="12dp"
android:maxHeight="@dimen/qs_media_enabled_seekbar_height"
android:splitTrack="false"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
app:layout_constraintStart_toEndOf="@id/actionPrev"
app:layout_constraintEnd_toStartOf="@id/actionNext"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/actionPlayPause" />
<ImageButton
android:id="@+id/actionNext"
style="@style/MediaPlayer.SessionAction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="@dimen/qs_media_action_spacing"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp"
app:layout_constraintStart_toEndOf="@id/media_progress_bar"
app:layout_constraintEnd_toStartOf="@id/actionStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/actionPlayPause" />
<ImageButton
android:id="@+id/actionStart"
style="@style/MediaPlayer.SessionAction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/qs_media_action_spacing"
android:layout_marginEnd="@dimen/qs_media_action_spacing"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp"
app:layout_constraintStart_toEndOf="@id/actionNext"
app:layout_constraintEnd_toStartOf="@id/actionEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/actionPlayPause" />
<ImageButton
android:id="@+id/actionEnd"
style="@style/MediaPlayer.SessionAction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/qs_media_action_spacing"
android:layout_marginEnd="4dp"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/actionStart"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/actionPlayPause" />
<!-- Long press menu -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/qs_media_padding"
android:layout_marginStart="@dimen/qs_media_padding"
android:layout_marginEnd="@dimen/qs_media_padding"
android:id="@+id/remove_text"
android:fontFamily="@*android:string/config_headlineFontFamily"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:text="@string/controls_media_close_session"
android:gravity="center_horizontal|top"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/cancel" />
<FrameLayout
android:id="@+id/settings"
android:background="@drawable/qs_media_light_source"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/qs_media_padding"
android:layout_marginEnd="@dimen/qs_media_action_spacing"
android:layout_marginBottom="@dimen/qs_media_padding"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
app:layout_constraintHeight_min="@dimen/min_clickable_item_size"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/remove_text">
<TextView
android:id="@+id/settings_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
style="@style/MediaPlayer.OutlineButton"
android:text="@string/controls_media_settings_button" />
</FrameLayout>
<FrameLayout
android:id="@+id/cancel"
android:background="@drawable/qs_media_light_source"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/qs_media_action_spacing"
android:layout_marginEnd="@dimen/qs_media_action_spacing"
android:layout_marginBottom="@dimen/qs_media_padding"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
app:layout_constraintHeight_min="@dimen/min_clickable_item_size"
app:layout_constraintStart_toEndOf="@id/settings"
app:layout_constraintEnd_toStartOf="@id/dismiss"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/remove_text">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
style="@style/MediaPlayer.OutlineButton"
android:text="@string/cancel" />
</FrameLayout>
<FrameLayout
android:id="@+id/dismiss"
android:background="@drawable/qs_media_light_source"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/qs_media_action_spacing"
android:layout_marginEnd="@dimen/qs_media_padding"
android:layout_marginBottom="@dimen/qs_media_padding"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
app:layout_constraintHeight_min="@dimen/min_clickable_item_size"
app:layout_constraintStart_toEndOf="@id/cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/remove_text">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
style="@style/MediaPlayer.OutlineButton"
android:text="@string/controls_media_dismiss_button" />
</FrameLayout>
</com.android.systemui.util.animation.TransitionLayout>

View File

@@ -969,6 +969,10 @@
<dimen name="qs_media_enabled_seekbar_vertical_padding">28dp</dimen>
<dimen name="qs_media_disabled_seekbar_vertical_padding">29dp</dimen>
<!-- Sizes for alternate session-based layout -->
<dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen>
<dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen>
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
<dimen name="qs_aa_media_rec_album_size_collapsed">72dp</dimen>
<dimen name="qs_aa_media_rec_album_size_expanded">76dp</dimen>

View File

@@ -581,6 +581,23 @@
<item name="android:scaleType">centerInside</item>
</style>
<style name="MediaPlayer.SessionAction"
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>
<item name="android:stateListAnimator">@anim/media_button_state_list_animator</item>
<item name="android:paddingTop">12dp</item>
<item name="android:paddingStart">12dp</item>
<item name="android:paddingEnd">12dp</item>
<item name="android:paddingBottom">12dp</item>
<item name="android:scaleType">centerInside</item>
</style>
<style name="MediaPlayer.SessionAction.Primary" parent="MediaPlayer.SessionAction">
<item name="android:background">@drawable/qs_media_round_button_background</item>
<item name="android:backgroundTint">@color/media_player_solid_button_bg</item>
</style>
<style name="MediaPlayer.OutlineButton">
<item name="android:background">@drawable/qs_media_button_background</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>

View File

@@ -18,6 +18,7 @@ import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
@@ -56,7 +57,8 @@ class MediaCarouselController @Inject constructor(
configurationController: ConfigurationController,
falsingCollector: FalsingCollector,
falsingManager: FalsingManager,
dumpManager: DumpManager
dumpManager: DumpManager,
private val mediaFlags: MediaFlags
) : Dumpable {
/**
* The current width of the carousel
@@ -382,7 +384,7 @@ class MediaCarouselController @Inject constructor(
private fun reorderAllPlayers(previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?) {
mediaContent.removeAllViews()
for (mediaPlayer in MediaPlayerData.players()) {
mediaPlayer.playerViewHolder?.let {
mediaPlayer.mediaViewHolder?.let {
mediaContent.addView(it.player)
} ?: mediaPlayer.recommendationViewHolder?.let {
mediaContent.addView(it.recommendations)
@@ -416,12 +418,19 @@ class MediaCarouselController @Inject constructor(
.elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
if (existingPlayer == null) {
var newPlayer = mediaControlPanelFactory.get()
newPlayer.attachPlayer(
PlayerViewHolder.create(LayoutInflater.from(context), mediaContent))
if (mediaFlags.areMediaSessionActionsEnabled()) {
newPlayer.attachPlayer(
PlayerSessionViewHolder.create(LayoutInflater.from(context), mediaContent),
MediaViewController.TYPE.PLAYER_SESSION)
} else {
newPlayer.attachPlayer(
PlayerViewHolder.create(LayoutInflater.from(context), mediaContent),
MediaViewController.TYPE.PLAYER)
}
newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
newPlayer.playerViewHolder?.player?.setLayoutParams(lp)
newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
newPlayer.bindPlayer(dataCopy, key)
newPlayer.setListening(currentlyExpanded)
MediaPlayerData.addMediaPlayer(key, dataCopy, newPlayer, systemClock)
@@ -493,7 +502,7 @@ class MediaCarouselController @Inject constructor(
val removed = MediaPlayerData.removeMediaPlayer(key)
removed?.apply {
mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
mediaContent.removeView(removed.playerViewHolder?.player)
mediaContent.removeView(removed.mediaViewHolder?.player)
mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
removed.onDestroy()
mediaCarouselScrollHandler.onPlayersChanged()
@@ -836,7 +845,7 @@ class MediaCarouselController @Inject constructor(
MediaPlayerData.players().forEachIndexed {
index, it ->
if (it.mIsImpressed) {
logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
it.mInstanceId,
it.mUid,
it.recommendationViewHolder != null,

View File

@@ -528,7 +528,7 @@ class MediaCarouselScrollHandler(
* where it was and update our scroll position.
*/
fun onPrePlayerRemoved(removed: MediaControlPanel) {
val removedIndex = mediaContent.indexOfChild(removed.playerViewHolder?.player)
val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player)
// If the removed index is less than the visibleMediaIndex, then we need to decrement it.
// RTL has no effect on this, because indices are always relative (start-to-end).
// Update the index 'manually' since we won't always get a call to onMediaScrollingChanged

View File

@@ -57,12 +57,10 @@ import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.time.SystemClock;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -87,6 +85,10 @@ public class MediaControlPanel {
private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name";
private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
// Event types logged by smartspace
private static final int SMARTSPACE_CARD_CLICK_EVENT = 760;
protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761;
private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
// Button IDs for QS controls
@@ -104,13 +106,12 @@ public class MediaControlPanel {
private final ActivityStarter mActivityStarter;
private Context mContext;
private PlayerViewHolder mPlayerViewHolder;
private MediaViewHolder mMediaViewHolder;
private RecommendationViewHolder mRecommendationViewHolder;
private String mKey;
private MediaViewController mMediaViewController;
private MediaSession.Token mToken;
private MediaController mController;
private KeyguardDismissUtil mKeyguardDismissUtil;
private Lazy<MediaDataManager> mMediaDataManagerLazy;
private int mBackgroundColor;
private int mDevicePadding;
@@ -139,8 +140,8 @@ public class MediaControlPanel {
public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
ActivityStarter activityStarter, MediaViewController mediaViewController,
SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
mediaOutputDialogFactory, MediaCarouselController mediaCarouselController,
MediaOutputDialogFactory mediaOutputDialogFactory,
MediaCarouselController mediaCarouselController,
FalsingManager falsingManager, MediaFlags mediaFlags, SystemClock systemClock) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
@@ -148,7 +149,6 @@ public class MediaControlPanel {
mSeekBarViewModel = seekBarViewModel;
mMediaViewController = mediaViewController;
mMediaDataManagerLazy = lazyMediaDataManager;
mKeyguardDismissUtil = keyguardDismissUtil;
mMediaOutputDialogFactory = mediaOutputDialogFactory;
mMediaCarouselController = mediaCarouselController;
mFalsingManager = falsingManager;
@@ -157,8 +157,7 @@ public class MediaControlPanel {
loadDimens();
mSeekBarViewModel.setLogSmartspaceClick(() -> {
logSmartspaceCardReported(
760, // SMARTSPACE_CARD_CLICK
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
/* isRecommendationCard */ false);
return Unit.INSTANCE;
});
@@ -179,13 +178,13 @@ public class MediaControlPanel {
}
/**
* Get the player view holder used to display media controls.
* Get the view holder used to display media controls.
*
* @return the player view holder
* @return the media view holder
*/
@Nullable
public PlayerViewHolder getPlayerViewHolder() {
return mPlayerViewHolder;
public MediaViewHolder getMediaViewHolder() {
return mMediaViewHolder;
}
/**
@@ -229,16 +228,17 @@ public class MediaControlPanel {
}
/** Attaches the player to the player view holder. */
public void attachPlayer(PlayerViewHolder vh) {
mPlayerViewHolder = vh;
public void attachPlayer(MediaViewHolder vh, MediaViewController.TYPE playerType) {
mMediaViewHolder = vh;
TransitionLayout player = vh.getPlayer();
mSeekBarObserver = new SeekBarObserver(vh);
boolean useSessionLayout = playerType == MediaViewController.TYPE.PLAYER_SESSION;
mSeekBarObserver = new SeekBarObserver(vh, useSessionLayout);
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
mMediaViewController.attach(player, playerType);
mPlayerViewHolder.getPlayer().setOnLongClickListener(v -> {
vh.getPlayer().setOnLongClickListener(v -> {
if (!mMediaViewController.isGutsVisible()) {
openGuts();
return true;
@@ -247,12 +247,12 @@ public class MediaControlPanel {
return true;
}
});
mPlayerViewHolder.getCancel().setOnClickListener(v -> {
vh.getCancel().setOnClickListener(v -> {
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
closeGuts();
}
});
mPlayerViewHolder.getSettings().setOnClickListener(v -> {
vh.getSettings().setOnClickListener(v -> {
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
}
@@ -289,9 +289,19 @@ public class MediaControlPanel {
/** Bind this player view based on the data given. */
public void bindPlayer(@NonNull MediaData data, String key) {
if (mPlayerViewHolder == null) {
if (mMediaViewHolder == null) {
return;
}
bindPlayerCommon(data, key);
if (mMediaViewHolder instanceof PlayerViewHolder) {
bindNotificationPlayer(data, key);
} else if (mMediaViewHolder instanceof PlayerSessionViewHolder) {
bindSessionPlayer(data, key);
}
}
/** Bind elements common to both layouts */
private void bindPlayerCommon(@NonNull MediaData data, String key) {
mKey = key;
MediaSession.Token token = data.getToken();
PackageManager packageManager = mContext.getPackageManager();
@@ -316,99 +326,63 @@ public class MediaControlPanel {
mController = null;
}
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
// Click action
PendingIntent clickIntent = data.getClickIntent();
if (clickIntent != null) {
mPlayerViewHolder.getPlayer().setOnClickListener(v -> {
mMediaViewHolder.getPlayer().setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
if (mMediaViewController.isGutsVisible()) return;
logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
/* isRecommendationCard */ false);
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
buildLaunchAnimatorController(mPlayerViewHolder.getPlayer()));
buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
});
}
// Accessibility label
mPlayerViewHolder.getPlayer().setContentDescription(
mMediaViewHolder.getPlayer().setContentDescription(
mContext.getString(
R.string.controls_media_playing_item_description,
data.getSong(), data.getArtist(), data.getApp()));
ImageView albumView = mPlayerViewHolder.getAlbumView();
boolean hasArtwork = data.getArtwork() != null;
if (hasArtwork) {
Drawable artwork = scaleDrawable(data.getArtwork());
albumView.setPadding(0, 0, 0, 0);
albumView.setImageDrawable(artwork);
} else {
Drawable deviceIcon;
if (data.getDevice() != null && data.getDevice().getIcon() != null) {
deviceIcon = data.getDevice().getIcon().getConstantState().newDrawable().mutate();
} else {
deviceIcon = getContext().getDrawable(R.drawable.ic_headphone);
}
deviceIcon.setTintList(ColorStateList.valueOf(mBackgroundColor));
albumView.setPadding(mDevicePadding, mDevicePadding, mDevicePadding, mDevicePadding);
albumView.setImageDrawable(deviceIcon);
}
// App icon
ImageView appIconView = mPlayerViewHolder.getAppIcon();
appIconView.clearColorFilter();
if (data.getAppIcon() != null && !data.getResumption()) {
appIconView.setImageIcon(data.getAppIcon());
int color = mContext.getColor(android.R.color.system_accent2_900);
appIconView.setColorFilter(color);
} else {
appIconView.setColorFilter(getGrayscaleFilter());
try {
Drawable icon = mContext.getPackageManager().getApplicationIcon(
data.getPackageName());
appIconView.setImageDrawable(icon);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
appIconView.setImageResource(R.drawable.ic_music_note);
}
}
// Song name
TextView titleText = mPlayerViewHolder.getTitleText();
TextView titleText = mMediaViewHolder.getTitleText();
titleText.setText(data.getSong());
// Artist name
TextView artistText = mPlayerViewHolder.getArtistText();
TextView artistText = mMediaViewHolder.getArtistText();
artistText.setText(data.getArtist());
// Transfer chip
ViewGroup seamlessView = mPlayerViewHolder.getSeamless();
// Seek Bar
final MediaController controller = getController();
mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
// Guts label
boolean isDismissible = data.isClearable();
mMediaViewHolder.getLongPressText().setText(isDismissible
? R.string.controls_media_close_session
: R.string.controls_media_active_session);
// Output switcher chip
ViewGroup seamlessView = mMediaViewHolder.getSeamless();
seamlessView.setVisibility(View.VISIBLE);
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
seamlessView.setOnClickListener(
v -> {
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
mMediaOutputDialogFactory.create(data.getPackageName(), true,
mPlayerViewHolder.getSeamlessButton());
mMediaViewHolder.getSeamlessButton());
}
});
ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
TextView deviceName = mPlayerViewHolder.getSeamlessText();
ImageView iconView = mMediaViewHolder.getSeamlessIcon();
TextView deviceName = mMediaViewHolder.getSeamlessText();
final MediaDeviceData device = data.getDevice();
final int seamlessId = mPlayerViewHolder.getSeamless().getId();
// Disable clicking on output switcher for invalid devices and resumption controls
final boolean seamlessDisabled = (device != null && !device.getEnabled())
|| data.getResumption();
final float seamlessAlpha = seamlessDisabled ? DISABLED_ALPHA : 1.0f;
expandedSet.setAlpha(seamlessId, seamlessAlpha);
collapsedSet.setAlpha(seamlessId, seamlessAlpha);
mPlayerViewHolder.getSeamless().setEnabled(!seamlessDisabled);
mMediaViewHolder.getSeamlessButton().setAlpha(seamlessAlpha);
seamlessView.setEnabled(!seamlessDisabled);
String deviceString = null;
if (device != null && device.getEnabled()) {
Drawable icon = device.getIcon();
@@ -429,32 +403,88 @@ public class MediaControlPanel {
deviceName.setText(deviceString);
seamlessView.setContentDescription(deviceString);
// Dismiss
mMediaViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
mMediaViewHolder.getDismiss().setEnabled(isDismissible);
mMediaViewHolder.getDismiss().setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
/* isRecommendationCard */ false);
if (mKey != null) {
closeGuts();
if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
Log.w(TAG, "Manager failed to dismiss media " + mKey);
// Remove directly from carousel so user isn't stuck with defunct controls
mMediaCarouselController.removePlayer(key, false, false);
}
} else {
Log.w(TAG, "Dismiss media with null notification. Token uid="
+ data.getToken().getUid());
}
});
// TODO: We don't need to refresh this state constantly, only if the state actually changed
// to something which might impact the measurement
mMediaViewController.refreshState();
}
/** Bind elements specific to PlayerViewHolder */
private void bindNotificationPlayer(@NonNull MediaData data, String key) {
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
// Album art
PlayerViewHolder playerHolder = (PlayerViewHolder) mMediaViewHolder;
ImageView albumView = playerHolder.getAlbumView();
boolean hasArtwork = data.getArtwork() != null;
if (hasArtwork) {
Drawable artwork = scaleDrawable(data.getArtwork());
albumView.setPadding(0, 0, 0, 0);
albumView.setImageDrawable(artwork);
} else {
Drawable deviceIcon;
if (data.getDevice() != null && data.getDevice().getIcon() != null) {
deviceIcon = data.getDevice().getIcon().getConstantState().newDrawable().mutate();
} else {
deviceIcon = getContext().getDrawable(R.drawable.ic_headphone);
}
deviceIcon.setTintList(ColorStateList.valueOf(mBackgroundColor));
albumView.setPadding(mDevicePadding, mDevicePadding, mDevicePadding, mDevicePadding);
albumView.setImageDrawable(deviceIcon);
}
// App icon - use notification icon
ImageView appIconView = mMediaViewHolder.getAppIcon();
appIconView.clearColorFilter();
if (data.getAppIcon() != null && !data.getResumption()) {
appIconView.setImageIcon(data.getAppIcon());
int color = mContext.getColor(android.R.color.system_accent2_900);
appIconView.setColorFilter(color);
} else {
// Resume players use launcher icon
appIconView.setColorFilter(getGrayscaleFilter());
try {
Drawable icon = mContext.getPackageManager().getApplicationIcon(
data.getPackageName());
appIconView.setImageDrawable(icon);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
appIconView.setImageResource(R.drawable.ic_music_note);
}
}
// Media action buttons
List<MediaAction> actionIcons = data.getActions();
List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
if (mMediaFlags.areMediaSessionActionsEnabled() && data.getSemanticActions() != null) {
// Use PlaybackState actions instead
MediaButton semanticActions = data.getSemanticActions();
actionIcons = new ArrayList<MediaAction>();
actionIcons.add(semanticActions.getStartCustom());
actionIcons.add(semanticActions.getPrevOrCustom());
actionIcons.add(semanticActions.getPlayOrPause());
actionIcons.add(semanticActions.getNextOrCustom());
actionIcons.add(semanticActions.getEndCustom());
actionsWhenCollapsed = new ArrayList<Integer>();
actionsWhenCollapsed.add(1);
actionsWhenCollapsed.add(2);
actionsWhenCollapsed.add(3);
}
int i = 0;
for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
int actionId = ACTION_IDS[i];
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
final ImageButton button = mPlayerViewHolder.getAction(actionId);
final ImageButton button = mMediaViewHolder.getAction(actionId);
MediaAction mediaAction = actionIcons.get(i);
if (mediaAction != null) {
button.setImageIcon(mediaAction.getIcon());
@@ -467,7 +497,7 @@ public class MediaControlPanel {
button.setEnabled(true);
button.setOnClickListener(v -> {
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
/* isRecommendationCard */ false);
action.run();
}
@@ -495,43 +525,71 @@ public class MediaControlPanel {
if (actionIcons.size() == 0) {
expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.INVISIBLE);
}
}
// Seek Bar
final MediaController controller = getController();
mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
// Guts label
boolean isDismissible = data.isClearable();
mPlayerViewHolder.getLongPressText().setText(isDismissible
? R.string.controls_media_close_session
: R.string.controls_media_active_session);
// Dismiss
mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
mPlayerViewHolder.getDismiss().setEnabled(isDismissible);
mPlayerViewHolder.getDismiss().setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
/* isRecommendationCard */ false);
if (mKey != null) {
closeGuts();
if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
Log.w(TAG, "Manager failed to dismiss media " + mKey);
// Remove directly from carousel to let user recover - TODO(b/190799184)
mMediaCarouselController.removePlayer(key, false, false);
}
/** Bind elements specific to PlayerSessionViewHolder */
private void bindSessionPlayer(@NonNull MediaData data, String key) {
// App icon - use launcher icon
ImageView appIconView = mMediaViewHolder.getAppIcon();
appIconView.clearColorFilter();
try {
Drawable icon = mContext.getPackageManager().getApplicationIcon(
data.getPackageName());
appIconView.setImageDrawable(icon);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
// Fall back to notification icon
if (data.getAppIcon() != null) {
appIconView.setImageIcon(data.getAppIcon());
} else {
Log.w(TAG, "Dismiss media with null notification. Token uid="
+ data.getToken().getUid());
appIconView.setImageResource(R.drawable.ic_music_note);
}
});
int color = mContext.getColor(android.R.color.system_accent2_900);
appIconView.setColorFilter(color);
}
// TODO: We don't need to refresh this state constantly, only if the state actually changed
// to something which might impact the measurement
mMediaViewController.refreshState();
// Media action buttons
MediaButton semanticActions = data.getSemanticActions();
if (semanticActions != null) {
PlayerSessionViewHolder sessionHolder = (PlayerSessionViewHolder) mMediaViewHolder;
setSemanticButton(sessionHolder.getActionPlayPause(),
semanticActions.getPlayOrPause());
setSemanticButton(sessionHolder.getActionNext(),
semanticActions.getNextOrCustom());
setSemanticButton(sessionHolder.getActionPrev(),
semanticActions.getPrevOrCustom());
setSemanticButton(sessionHolder.getActionStart(),
semanticActions.getStartCustom());
setSemanticButton(sessionHolder.getActionEnd(),
semanticActions.getEndCustom());
} else {
Log.w(TAG, "Using semantic player, but did not get buttons");
}
}
private void setSemanticButton(final ImageButton button, MediaAction mediaAction) {
if (mediaAction != null) {
button.setImageIcon(mediaAction.getIcon());
button.setContentDescription(mediaAction.getContentDescription());
Runnable action = mediaAction.getAction();
if (action == null) {
button.setEnabled(false);
} else {
button.setEnabled(true);
button.setOnClickListener(v -> {
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
/* isRecommendationCard */ false);
action.run();
}
});
}
} else {
button.setImageIcon(null);
button.setContentDescription(null);
button.setEnabled(false);
}
}
@Nullable
@@ -696,7 +754,7 @@ public class MediaControlPanel {
mRecommendationViewHolder.getDismiss().setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
/* isRecommendationCard */ true);
closeGuts();
mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
@@ -729,8 +787,8 @@ public class MediaControlPanel {
* @param immediate {@code true} if it should be closed without animation
*/
public void closeGuts(boolean immediate) {
if (mPlayerViewHolder != null) {
mPlayerViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
if (mMediaViewHolder != null) {
mMediaViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
} else if (mRecommendationViewHolder != null) {
mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
}
@@ -747,9 +805,9 @@ public class MediaControlPanel {
boolean wasTruncated = false;
Layout l = null;
if (mPlayerViewHolder != null) {
mPlayerViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
l = mPlayerViewHolder.getSettingsText().getLayout();
if (mMediaViewHolder != null) {
mMediaViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
l = mMediaViewHolder.getSettingsText().getLayout();
} else if (mRecommendationViewHolder != null) {
mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
l = mRecommendationViewHolder.getSettingsText().getLayout();
@@ -853,7 +911,7 @@ public class MediaControlPanel {
view.setOnClickListener(v -> {
if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
/* isRecommendationCard */ true,
interactedSubcardRank,
getSmartspaceSubCardCardinality());

View File

@@ -37,9 +37,12 @@ class MediaViewController @Inject constructor(
private val mediaHostStatesManager: MediaHostStatesManager
) {
/** Indicating the media view controller is for a player or recommendation. */
/**
* Indicating that the media view controller is for a notification-based player,
* session-based player, or recommendation
*/
enum class TYPE {
PLAYER, RECOMMENDATION
PLAYER, PLAYER_SESSION, RECOMMENDATION
}
companion object {
@@ -257,31 +260,29 @@ class MediaViewController @Inject constructor(
* [TransitionViewState].
*/
private fun setGutsViewState(viewState: TransitionViewState) {
if (type == TYPE.PLAYER) {
PlayerViewHolder.controlsIds.forEach { id ->
viewState.widgetStates.get(id)?.let { state ->
// Make sure to use the unmodified state if guts are not visible.
state.alpha = if (isGutsVisible) 0f else state.alpha
state.gone = if (isGutsVisible) true else state.gone
}
}
PlayerViewHolder.gutsIds.forEach { id ->
viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f
viewState.widgetStates.get(id)?.gone = !isGutsVisible
}
} else {
RecommendationViewHolder.controlsIds.forEach { id ->
viewState.widgetStates.get(id)?.let { state ->
// Make sure to use the unmodified state if guts are not visible.
state.alpha = if (isGutsVisible) 0f else state.alpha
state.gone = if (isGutsVisible) true else state.gone
}
}
RecommendationViewHolder.gutsIds.forEach { id ->
viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f
viewState.widgetStates.get(id)?.gone = !isGutsVisible
val controlsIds = when (type) {
TYPE.PLAYER -> PlayerViewHolder.controlsIds
TYPE.PLAYER_SESSION -> PlayerSessionViewHolder.controlsIds
TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
}
val gutsIds = when (type) {
TYPE.PLAYER -> PlayerViewHolder.gutsIds
TYPE.PLAYER_SESSION -> PlayerSessionViewHolder.gutsIds
TYPE.RECOMMENDATION -> RecommendationViewHolder.gutsIds
}
controlsIds.forEach { id ->
viewState.widgetStates.get(id)?.let { state ->
// Make sure to use the unmodified state if guts are not visible.
state.alpha = if (isGutsVisible) 0f else state.alpha
state.gone = if (isGutsVisible) true else state.gone
}
}
gutsIds.forEach { id ->
viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f
viewState.widgetStates.get(id)?.gone = !isGutsVisible
}
if (shouldHideGutsSettings) {
viewState.widgetStates.get(R.id.settings)?.gone = true
}
@@ -470,12 +471,19 @@ class MediaViewController @Inject constructor(
private fun updateMediaViewControllerType(type: TYPE) {
this.type = type
if (type == TYPE.PLAYER) {
collapsedLayout.load(context, R.xml.media_collapsed)
expandedLayout.load(context, R.xml.media_expanded)
} else {
collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
expandedLayout.load(context, R.xml.media_recommendation_expanded)
when (type) {
TYPE.PLAYER -> {
collapsedLayout.load(context, R.xml.media_collapsed)
expandedLayout.load(context, R.xml.media_expanded)
}
TYPE.PLAYER_SESSION -> {
collapsedLayout.clone(context, R.layout.media_session_view)
expandedLayout.clone(context, R.layout.media_session_view)
}
TYPE.RECOMMENDATION -> {
collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
expandedLayout.load(context, R.xml.media_recommendation_expanded)
}
}
refreshState()
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "MediaViewHolder"
/**
* Parent class for different media player views
*/
abstract class MediaViewHolder constructor(itemView: View) {
val player = itemView as TransitionLayout
// Player information
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
// Output switcher
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
// Seekbar views
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
open val elapsedTimeView: TextView? = null
open val totalTimeView: TextView? = null
// Settings screen
val longPressText = itemView.requireViewById<TextView>(R.id.remove_text)
val cancel = itemView.requireViewById<View>(R.id.cancel)
val dismiss = itemView.requireViewById<ViewGroup>(R.id.dismiss)
val dismissLabel = dismiss.getChildAt(0)
val settings = itemView.requireViewById<View>(R.id.settings)
val settingsText = itemView.requireViewById<TextView>(R.id.settings_text)
init {
(player.background as IlluminationDrawable).let {
it.registerLightSource(seamless)
it.registerLightSource(cancel)
it.registerLightSource(dismiss)
it.registerLightSource(settings)
}
}
abstract fun getAction(id: Int): ImageButton
fun marquee(start: Boolean, delay: Long) {
val longPressTextHandler = longPressText.getHandler()
if (longPressTextHandler == null) {
Log.d(TAG, "marquee while longPressText.getHandler() is null", Exception())
return
}
longPressTextHandler.postDelayed({ longPressText.setSelected(start) }, delay)
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import com.android.systemui.R
/**
* ViewHolder for a media player with MediaSession-based controls
*/
class PlayerSessionViewHolder private constructor(itemView: View) : MediaViewHolder(itemView) {
// Action Buttons
val actionPlayPause = itemView.requireViewById<ImageButton>(R.id.actionPlayPause)
val actionNext = itemView.requireViewById<ImageButton>(R.id.actionNext)
val actionPrev = itemView.requireViewById<ImageButton>(R.id.actionPrev)
val actionStart = itemView.requireViewById<ImageButton>(R.id.actionStart)
val actionEnd = itemView.requireViewById<ImageButton>(R.id.actionEnd)
init {
(player.background as IlluminationDrawable).let {
it.registerLightSource(actionPlayPause)
it.registerLightSource(actionNext)
it.registerLightSource(actionPrev)
it.registerLightSource(actionStart)
it.registerLightSource(actionEnd)
}
}
override fun getAction(id: Int): ImageButton {
return when (id) {
R.id.actionPlayPause -> actionPlayPause
R.id.actionNext -> actionNext
R.id.actionPrev -> actionPrev
R.id.actionStart -> actionStart
R.id.actionEnd -> actionEnd
else -> {
throw IllegalArgumentException()
}
}
}
companion object {
/**
* Creates a PlayerSessionViewHolder.
*
* @param inflater LayoutInflater to use to inflate the layout.
* @param parent Parent of inflated view.
*/
@JvmStatic fun create(
inflater: LayoutInflater,
parent: ViewGroup
): PlayerSessionViewHolder {
val mediaView = inflater.inflate(R.layout.media_session_view, parent, false)
mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// Because this media view (a TransitionLayout) is used to measure and layout the views
// in various states before being attached to its parent, we can't depend on the default
// LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
mediaView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
return PlayerSessionViewHolder(mediaView).apply {
// Media playback is in the direction of tape, not time, so it stays LTR
seekBar.layoutDirection = View.LAYOUT_DIRECTION_LTR
}
}
val controlsIds = setOf(
R.id.icon,
R.id.app_name,
R.id.header_title,
R.id.header_artist,
R.id.media_seamless,
R.id.media_progress_bar,
R.id.actionPlayPause,
R.id.actionNext,
R.id.actionPrev,
R.id.actionStart,
R.id.actionEnd,
R.id.icon
)
val gutsIds = setOf(
R.id.remove_text,
R.id.cancel,
R.id.dismiss,
R.id.settings
)
}
}

View File

@@ -16,43 +16,26 @@
package com.android.systemui.media
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "PlayerViewHolder"
/**
* ViewHolder for a media player.
*/
class PlayerViewHolder private constructor(itemView: View) {
val player = itemView as TransitionLayout
class PlayerViewHolder private constructor(itemView: View) : MediaViewHolder(itemView) {
// Player information
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
// Output switcher
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
// Seek bar
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
val progressTimes = itemView.requireViewById<ViewGroup>(R.id.notification_media_progress_time)
val elapsedTimeView = itemView.requireViewById<TextView>(R.id.media_elapsed_time)
val totalTimeView = itemView.requireViewById<TextView>(R.id.media_total_time)
override val elapsedTimeView = itemView.requireViewById<TextView>(R.id.media_elapsed_time)
override val totalTimeView = itemView.requireViewById<TextView>(R.id.media_total_time)
// Action Buttons
val action0 = itemView.requireViewById<ImageButton>(R.id.action0)
@@ -61,29 +44,17 @@ class PlayerViewHolder private constructor(itemView: View) {
val action3 = itemView.requireViewById<ImageButton>(R.id.action3)
val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
// Settings screen
val longPressText = itemView.requireViewById<TextView>(R.id.remove_text)
val cancel = itemView.requireViewById<View>(R.id.cancel)
val dismiss = itemView.requireViewById<ViewGroup>(R.id.dismiss)
val dismissLabel = dismiss.getChildAt(0)
val settings = itemView.requireViewById<View>(R.id.settings)
val settingsText = itemView.requireViewById<TextView>(R.id.settings_text)
init {
(player.background as IlluminationDrawable).let {
it.registerLightSource(seamless)
it.registerLightSource(action0)
it.registerLightSource(action1)
it.registerLightSource(action2)
it.registerLightSource(action3)
it.registerLightSource(action4)
it.registerLightSource(cancel)
it.registerLightSource(dismiss)
it.registerLightSource(settings)
}
}
fun getAction(id: Int): ImageButton {
override fun getAction(id: Int): ImageButton {
return when (id) {
R.id.action0 -> action0
R.id.action1 -> action1
@@ -96,15 +67,6 @@ class PlayerViewHolder private constructor(itemView: View) {
}
}
fun marquee(start: Boolean, delay: Long) {
val longPressTextHandler = longPressText.getHandler()
if (longPressTextHandler == null) {
Log.d(TAG, "marquee while longPressText.getHandler() is null", Exception())
return
}
longPressTextHandler.postDelayed({ longPressText.setSelected(start) }, delay)
}
companion object {
/**
* Creates a PlayerViewHolder.

View File

@@ -26,16 +26,29 @@ import com.android.systemui.R
*
* <p>Updates the seek bar views in response to changes to the model.
*/
class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarViewModel.Progress> {
class SeekBarObserver(
private val holder: MediaViewHolder,
private val useSessionLayout: Boolean
) : Observer<SeekBarViewModel.Progress> {
val seekBarEnabledMaxHeight = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height)
val seekBarDisabledHeight = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height)
val seekBarEnabledVerticalPadding = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_vertical_padding)
val seekBarDisabledVerticalPadding = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_vertical_padding)
val seekBarEnabledVerticalPadding = if (useSessionLayout) {
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_session_enabled_seekbar_vertical_padding)
} else {
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_vertical_padding)
}
val seekBarDisabledVerticalPadding = if (useSessionLayout) {
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_session_disabled_seekbar_vertical_padding)
} else {
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_vertical_padding)
}
/** Updates seek bar views when the data model changes. */
@UiThread
@@ -48,8 +61,8 @@ class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarVi
holder.seekBar.setEnabled(false)
holder.seekBar.getThumb().setAlpha(0)
holder.seekBar.setProgress(0)
holder.elapsedTimeView.setText("")
holder.totalTimeView.setText("")
holder.elapsedTimeView?.setText("")
holder.totalTimeView?.setText("")
holder.seekBar.contentDescription = ""
return
}
@@ -65,13 +78,13 @@ class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarVi
holder.seekBar.setMax(data.duration)
val totalTimeString = DateUtils.formatElapsedTime(
data.duration / DateUtils.SECOND_IN_MILLIS)
holder.totalTimeView.setText(totalTimeString)
holder.totalTimeView?.setText(totalTimeString)
data.elapsedTime?.let {
holder.seekBar.setProgress(it)
val elapsedTimeString = DateUtils.formatElapsedTime(
it / DateUtils.SECOND_IN_MILLIS)
holder.elapsedTimeView.setText(elapsedTimeString)
holder.elapsedTimeView?.setText(elapsedTimeString)
holder.seekBar.contentDescription = holder.seekBar.context.getString(
R.string.controls_media_seekbar_description,

View File

@@ -36,6 +36,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
import javax.inject.Provider
private val DATA = MediaData(
@@ -74,6 +75,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Mock lateinit var falsingCollector: FalsingCollector
@Mock lateinit var falsingManager: FalsingManager
@Mock lateinit var dumpManager: DumpManager
@Mock lateinit var mediaFlags: MediaFlags
private val clock = FakeSystemClock()
private lateinit var mediaCarouselController: MediaCarouselController
@@ -81,7 +83,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(true)
mediaCarouselController = MediaCarouselController(
context,
mediaControlPanelFactory,
@@ -94,7 +96,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
configurationController,
falsingCollector,
falsingManager,
dumpManager
dumpManager,
mediaFlags
)
MediaPlayerData.clear()

View File

@@ -43,7 +43,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
@@ -87,11 +86,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var holder: PlayerViewHolder
@Mock private lateinit var sessionHolder: PlayerSessionViewHolder
@Mock private lateinit var view: TransitionLayout
@Mock private lateinit var seekBarViewModel: SeekBarViewModel
@Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
@Mock private lateinit var mediaViewController: MediaViewController
@Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil
@Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var expandedSet: ConstraintSet
@Mock private lateinit var collapsedSet: ConstraintSet
@@ -104,6 +103,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var titleText: TextView
private lateinit var artistText: TextView
private lateinit var seamless: ViewGroup
private lateinit var seamlessButton: View
@Mock private lateinit var seamlessBackground: RippleDrawable
private lateinit var seamlessIcon: ImageView
private lateinit var seamlessText: TextView
private lateinit var seekBar: SeekBar
@@ -114,6 +115,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var action2: ImageButton
private lateinit var action3: ImageButton
private lateinit var action4: ImageButton
private lateinit var actionPlayPause: ImageButton
private lateinit var actionNext: ImageButton
private lateinit var actionPrev: ImageButton
private lateinit var actionStart: ImageButton
private lateinit var actionEnd: ImageButton
@Mock private lateinit var longPressText: TextView
@Mock private lateinit var handler: Handler
private lateinit var settings: View
@@ -137,62 +143,30 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
seekBarViewModel, Lazy { mediaDataManager },
mediaOutputDialogFactory, mediaCarouselController, falsingManager, mediaFlags, clock)
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Mock out a view holder for the player to attach to.
whenever(holder.player).thenReturn(view)
// Set up mock views for the players
appIcon = ImageView(context)
whenever(holder.appIcon).thenReturn(appIcon)
albumView = ImageView(context)
whenever(holder.albumView).thenReturn(albumView)
titleText = TextView(context)
whenever(holder.titleText).thenReturn(titleText)
artistText = TextView(context)
whenever(holder.artistText).thenReturn(artistText)
seamless = FrameLayout(context)
val seamlessBackground = mock(RippleDrawable::class.java)
seamless.foreground = seamlessBackground
whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
whenever(holder.seamless).thenReturn(seamless)
seamlessButton = View(context)
seamlessIcon = ImageView(context)
whenever(holder.seamlessIcon).thenReturn(seamlessIcon)
seamlessText = TextView(context)
whenever(holder.seamlessText).thenReturn(seamlessText)
seekBar = SeekBar(context)
whenever(holder.seekBar).thenReturn(seekBar)
elapsedTimeView = TextView(context)
whenever(holder.elapsedTimeView).thenReturn(elapsedTimeView)
totalTimeView = TextView(context)
whenever(holder.totalTimeView).thenReturn(totalTimeView)
action0 = ImageButton(context)
whenever(holder.action0).thenReturn(action0)
whenever(holder.getAction(R.id.action0)).thenReturn(action0)
action1 = ImageButton(context)
whenever(holder.action1).thenReturn(action1)
whenever(holder.getAction(R.id.action1)).thenReturn(action1)
action2 = ImageButton(context)
whenever(holder.action2).thenReturn(action2)
whenever(holder.getAction(R.id.action2)).thenReturn(action2)
action3 = ImageButton(context)
whenever(holder.action3).thenReturn(action3)
whenever(holder.getAction(R.id.action3)).thenReturn(action3)
action4 = ImageButton(context)
whenever(holder.action4).thenReturn(action4)
whenever(holder.getAction(R.id.action4)).thenReturn(action4)
whenever(holder.longPressText).thenReturn(longPressText)
whenever(longPressText.handler).thenReturn(handler)
settings = View(context)
whenever(holder.settings).thenReturn(settings)
settingsText = TextView(context)
whenever(holder.settingsText).thenReturn(settingsText)
cancel = View(context)
whenever(holder.cancel).thenReturn(cancel)
dismiss = FrameLayout(context)
whenever(holder.dismiss).thenReturn(dismiss)
dismissLabel = View(context)
whenever(holder.dismissLabel).thenReturn(dismissLabel)
initPlayerHolderMocks()
initSessionHolderMocks()
// Create media session
val metadataBuilder = MediaMetadata.Builder().apply {
@@ -230,6 +204,89 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(mediaFlags.areMediaSessionActionsEnabled()).thenReturn(false)
}
/** Mock view holder for the notification player */
private fun initPlayerHolderMocks() {
whenever(holder.player).thenReturn(view)
whenever(holder.appIcon).thenReturn(appIcon)
whenever(holder.albumView).thenReturn(albumView)
whenever(holder.titleText).thenReturn(titleText)
whenever(holder.artistText).thenReturn(artistText)
whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
whenever(holder.seamless).thenReturn(seamless)
whenever(holder.seamlessButton).thenReturn(seamlessButton)
whenever(holder.seamlessIcon).thenReturn(seamlessIcon)
whenever(holder.seamlessText).thenReturn(seamlessText)
whenever(holder.seekBar).thenReturn(seekBar)
whenever(holder.elapsedTimeView).thenReturn(elapsedTimeView)
whenever(holder.totalTimeView).thenReturn(totalTimeView)
// Action buttons
action0 = ImageButton(context)
whenever(holder.action0).thenReturn(action0)
whenever(holder.getAction(R.id.action0)).thenReturn(action0)
action1 = ImageButton(context)
whenever(holder.action1).thenReturn(action1)
whenever(holder.getAction(R.id.action1)).thenReturn(action1)
action2 = ImageButton(context)
whenever(holder.action2).thenReturn(action2)
whenever(holder.getAction(R.id.action2)).thenReturn(action2)
action3 = ImageButton(context)
whenever(holder.action3).thenReturn(action3)
whenever(holder.getAction(R.id.action3)).thenReturn(action3)
action4 = ImageButton(context)
whenever(holder.action4).thenReturn(action4)
whenever(holder.getAction(R.id.action4)).thenReturn(action4)
// Long press menu
whenever(holder.longPressText).thenReturn(longPressText)
whenever(longPressText.handler).thenReturn(handler)
whenever(holder.settings).thenReturn(settings)
whenever(holder.settingsText).thenReturn(settingsText)
whenever(holder.cancel).thenReturn(cancel)
whenever(holder.dismiss).thenReturn(dismiss)
whenever(holder.dismissLabel).thenReturn(dismissLabel)
}
/** Mock view holder for session player */
private fun initSessionHolderMocks() {
whenever(sessionHolder.player).thenReturn(view)
whenever(sessionHolder.appIcon).thenReturn(appIcon)
whenever(sessionHolder.titleText).thenReturn(titleText)
whenever(sessionHolder.artistText).thenReturn(artistText)
val seamlessBackground = mock(RippleDrawable::class.java)
whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
whenever(sessionHolder.seamless).thenReturn(seamless)
whenever(sessionHolder.seamlessButton).thenReturn(seamlessButton)
whenever(sessionHolder.seamlessIcon).thenReturn(seamlessIcon)
whenever(sessionHolder.seamlessText).thenReturn(seamlessText)
whenever(sessionHolder.seekBar).thenReturn(seekBar)
// Action buttons
actionPlayPause = ImageButton(context)
whenever(sessionHolder.actionPlayPause).thenReturn(actionPlayPause)
whenever(sessionHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause)
actionNext = ImageButton(context)
whenever(sessionHolder.actionNext).thenReturn(actionNext)
whenever(sessionHolder.getAction(R.id.actionNext)).thenReturn(actionNext)
actionPrev = ImageButton(context)
whenever(sessionHolder.actionPrev).thenReturn(actionPrev)
whenever(sessionHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev)
actionStart = ImageButton(context)
whenever(sessionHolder.actionStart).thenReturn(actionStart)
whenever(sessionHolder.getAction(R.id.actionStart)).thenReturn(actionStart)
actionEnd = ImageButton(context)
whenever(sessionHolder.actionEnd).thenReturn(actionEnd)
whenever(sessionHolder.getAction(R.id.actionEnd)).thenReturn(actionEnd)
// Long press menu
whenever(sessionHolder.longPressText).thenReturn(longPressText)
whenever(sessionHolder.settings).thenReturn(settings)
whenever(sessionHolder.settingsText).thenReturn(settingsText)
whenever(sessionHolder.cancel).thenReturn(cancel)
whenever(sessionHolder.dismiss).thenReturn(dismiss)
whenever(sessionHolder.dismissLabel).thenReturn(dismissLabel)
}
@After
fun tearDown() {
session.release()
@@ -255,32 +312,28 @@ public class MediaControlPanelTest : SysuiTestCase() {
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(holder)
player.attachPlayer(sessionHolder, MediaViewController.TYPE.PLAYER_SESSION)
player.bindPlayer(state, PACKAGE)
verify(expandedSet).setVisibility(R.id.action0, ConstraintSet.VISIBLE)
assertThat(action0.contentDescription).isEqualTo("custom 1")
assertThat(action0.isEnabled()).isFalse()
assertThat(actionStart.contentDescription).isEqualTo("custom 1")
assertThat(actionStart.isEnabled()).isFalse()
verify(expandedSet).setVisibility(R.id.action1, ConstraintSet.INVISIBLE)
assertThat(action1.isEnabled()).isFalse()
assertThat(actionPrev.isEnabled()).isFalse()
assertThat(actionPrev.drawable).isNull()
verify(expandedSet).setVisibility(R.id.action2, ConstraintSet.VISIBLE)
assertThat(action2.isEnabled()).isTrue()
assertThat(action2.contentDescription).isEqualTo("play")
assertThat(actionPlayPause.isEnabled()).isTrue()
assertThat(actionPlayPause.contentDescription).isEqualTo("play")
verify(expandedSet).setVisibility(R.id.action3, ConstraintSet.VISIBLE)
assertThat(action3.isEnabled()).isTrue()
assertThat(action3.contentDescription).isEqualTo("next")
assertThat(actionNext.isEnabled()).isTrue()
assertThat(actionNext.contentDescription).isEqualTo("next")
verify(expandedSet).setVisibility(R.id.action4, ConstraintSet.VISIBLE)
assertThat(action4.contentDescription).isEqualTo("custom 2")
assertThat(action4.isEnabled()).isFalse()
assertThat(actionEnd.contentDescription).isEqualTo("custom 2")
assertThat(actionEnd.isEnabled()).isFalse()
}
@Test
fun bindText() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
player.bindPlayer(mediaData, PACKAGE)
assertThat(titleText.getText()).isEqualTo(TITLE)
assertThat(artistText.getText()).isEqualTo(ARTIST)
@@ -288,7 +341,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindDevice() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
player.bindPlayer(mediaData, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.contentDescription).isEqualTo(DEVICE_NAME)
@@ -299,7 +352,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindDisabledDevice() {
seamless.id = 1
val fallbackString = context.getString(R.string.media_seamless_other_device)
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(device = disabledDevice)
player.bindPlayer(state, PACKAGE)
assertThat(seamless.isEnabled()).isFalse()
@@ -310,7 +363,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindNullDevice() {
val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(device = null)
player.bindPlayer(state, PACKAGE)
assertThat(seamless.isEnabled()).isTrue()
@@ -320,7 +373,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindDeviceResumptionPlayer() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(resumption = true)
player.bindPlayer(state, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
@@ -329,7 +382,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun longClick_gutsClosed() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
whenever(mediaViewController.isGutsVisible).thenReturn(false)
val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
@@ -341,7 +394,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun longClick_gutsOpen() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
whenever(mediaViewController.isGutsVisible).thenReturn(true)
val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
@@ -354,7 +407,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun cancelButtonClick_animation() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
cancel.callOnClick()
@@ -363,7 +416,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun settingsButtonClick() {
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
settings.callOnClick()
@@ -376,7 +429,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun dismissButtonClick() {
val mediaKey = "key for dismissal"
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(notificationKey = KEY)
player.bindPlayer(state, mediaKey)
@@ -388,7 +441,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun dismissButtonDisabled() {
val mediaKey = "key for dismissal"
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(isClearable = false, notificationKey = KEY)
player.bindPlayer(state, mediaKey)
@@ -400,7 +453,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
val mediaKey = "key for dismissal"
whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong())).thenReturn(false)
player.attachPlayer(holder)
player.attachPlayer(holder, MediaViewController.TYPE.PLAYER)
val state = mediaData.copy(notificationKey = KEY)
player.bindPlayer(state, mediaKey)

View File

@@ -62,7 +62,7 @@ public class SeekBarObserverTest : SysuiTestCase() {
whenever(mockHolder.elapsedTimeView).thenReturn(elapsedTimeView)
whenever(mockHolder.totalTimeView).thenReturn(totalTimeView)
observer = SeekBarObserver(mockHolder)
observer = SeekBarObserver(mockHolder, false /* useSessionLayout */)
}
@Test