Adjust the number of recommendations according to display size

We currently have 3 recommendations to show in the card. When the
display size gets bigger, the 3 recommendation covers cannot fit in the
media carousel. So we calculate the width of the screen compared to the
the default width of media controls. Then we get min(3, available width / cover)
as the number of recommendations that can be visibile.
Added changes to title/subtitle visibility that were missing from
collapsed layout.

Bug: 267295550
Test: atest MediaControlPanelTest
Test: checked recommendation card in both expanded and collapsed
layouts. And tried all combinations of display/text sizes.

Change-Id: Ic047c020fd91f9ed835619fe14748a7a40132798
This commit is contained in:
Michael Mikhail
2023-03-02 23:54:07 +00:00
parent 03f1bb5062
commit 65c796644b
6 changed files with 170 additions and 77 deletions

View File

@@ -64,6 +64,10 @@
<!-- The number of rows in the QuickSettings -->
<integer name="quick_settings_max_rows">4</integer>
<!-- If the dp width of the available space is <= this value, potentially adjust the number
of media recommendation items-->
<integer name="default_qs_media_rec_width_dp">380</integer>
<!-- The number of columns that the top level tiles span in the QuickSettings -->
<!-- The default tiles to display in QuickSettings -->

View File

@@ -1094,6 +1094,7 @@
<dimen name="qs_media_session_collapsed_guideline">144dp</dimen>
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
<dimen name="qs_media_rec_default_width">380dp</dimen>
<dimen name="qs_media_rec_icon_top_margin">16dp</dimen>
<dimen name="qs_media_rec_album_icon_size">16dp</dimen>
<dimen name="qs_media_rec_album_size">88dp</dimen>

View File

@@ -109,10 +109,11 @@ constructor(
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
) : Dumpable {
/** The current width of the carousel */
private var currentCarouselWidth: Int = 0
var currentCarouselWidth: Int = 0
private set
/** The current height of the carousel */
@VisibleForTesting var currentCarouselHeight: Int = 0
private var currentCarouselHeight: Int = 0
/** Are we currently showing only active players */
private var currentlyShowingOnlyActive: Boolean = false

View File

@@ -32,6 +32,8 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Color;
@@ -54,6 +56,7 @@ import android.os.Process;
import android.os.Trace;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
@@ -468,6 +471,7 @@ public class MediaControlPanel {
TransitionLayout recommendations = vh.getRecommendations();
mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION);
mMediaViewController.configurationChangeListener = this::updateRecommendationsVisibility;
mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> {
if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
@@ -1364,6 +1368,7 @@ public class MediaControlPanel {
boolean hasTitle = false;
boolean hasSubtitle = false;
int fittedRecsNum = getNumberOfFittedRecommendations();
for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) {
SmartspaceAction recommendation = recommendations.get(itemIndex);
@@ -1444,12 +1449,20 @@ public class MediaControlPanel {
// If there's no subtitles and/or titles for any of the albums, hide those views.
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
final boolean titlesVisible = hasTitle;
final boolean subtitlesVisible = hasSubtitle;
mRecommendationViewHolder.getMediaTitles().forEach((titleView) ->
setVisibleAndAlpha(expandedSet, titleView.getId(), titlesVisible));
mRecommendationViewHolder.getMediaSubtitles().forEach((subtitleView) ->
setVisibleAndAlpha(expandedSet, subtitleView.getId(), subtitlesVisible));
mRecommendationViewHolder.getMediaTitles().forEach((titleView) -> {
setVisibleAndAlpha(expandedSet, titleView.getId(), titlesVisible);
setVisibleAndAlpha(collapsedSet, titleView.getId(), titlesVisible);
});
mRecommendationViewHolder.getMediaSubtitles().forEach((subtitleView) -> {
setVisibleAndAlpha(expandedSet, subtitleView.getId(), subtitlesVisible);
setVisibleAndAlpha(collapsedSet, subtitleView.getId(), subtitlesVisible);
});
// Media covers visibility.
setMediaCoversVisibility(fittedRecsNum);
// Guts
Runnable onDismissClickedRunnable = () -> {
@@ -1486,6 +1499,51 @@ public class MediaControlPanel {
Trace.endSection();
}
private Unit updateRecommendationsVisibility() {
int fittedRecsNum = getNumberOfFittedRecommendations();
setMediaCoversVisibility(fittedRecsNum);
return Unit.INSTANCE;
}
private void setMediaCoversVisibility(int fittedRecsNum) {
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers();
// Hide media cover that cannot fit in the recommendation card.
for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) {
setVisibleAndAlpha(expandedSet, mediaCoverContainers.get(itemIndex).getId(),
itemIndex < fittedRecsNum);
setVisibleAndAlpha(collapsedSet, mediaCoverContainers.get(itemIndex).getId(),
itemIndex < fittedRecsNum);
}
}
@VisibleForTesting
protected int getNumberOfFittedRecommendations() {
Resources res = mContext.getResources();
Configuration config = res.getConfiguration();
int defaultDpWidth = res.getInteger(R.integer.default_qs_media_rec_width_dp);
int recCoverWidth = res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width)
+ res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2;
// On landscape, media controls should take half of the screen width.
int displayAvailableDpWidth = config.screenWidthDp;
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
displayAvailableDpWidth = displayAvailableDpWidth / 2;
}
int fittedNum;
if (displayAvailableDpWidth > defaultDpWidth) {
int recCoverDefaultWidth = res.getDimensionPixelSize(
R.dimen.qs_media_rec_default_width);
fittedNum = recCoverDefaultWidth / recCoverWidth;
} else {
int displayAvailableWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
displayAvailableDpWidth, res.getDisplayMetrics());
fittedNum = displayAvailableWidth / recCoverWidth;
}
return Math.min(fittedNum, NUM_REQUIRED_RECOMMENDATIONS);
}
private void fetchAndUpdateRecommendationColors(Drawable appIcon) {
mBackgroundExecutor.execute(() -> {
ColorScheme colorScheme = new ColorScheme(

View File

@@ -96,6 +96,7 @@ constructor(
/** A listener when the current dimensions of the player change */
lateinit var sizeChangedListener: () -> Unit
lateinit var configurationChangeListener: () -> Unit
private var firstRefresh: Boolean = true
@VisibleForTesting private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController()
@@ -195,6 +196,10 @@ constructor(
)
}
}
if (this@MediaViewController::configurationChangeListener.isInitialized) {
configurationChangeListener.invoke()
refreshState()
}
}
}
}

View File

@@ -209,12 +209,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var coverContainer3: ViewGroup
@Mock private lateinit var recAppIconItem: CachingIconView
@Mock private lateinit var recCardTitle: TextView
@Mock private lateinit var recProgressBar1: SeekBar
@Mock private lateinit var recProgressBar2: SeekBar
@Mock private lateinit var recProgressBar3: SeekBar
@Mock private lateinit var recSubtitleMock1: TextView
@Mock private lateinit var recSubtitleMock2: TextView
@Mock private lateinit var recSubtitleMock3: TextView
@Mock private lateinit var coverItem: ImageView
@Mock private lateinit var matrix: Matrix
private lateinit var coverItem1: ImageView
@@ -226,6 +220,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var recSubtitle1: TextView
private lateinit var recSubtitle2: TextView
private lateinit var recSubtitle3: TextView
@Mock private lateinit var recProgressBar1: SeekBar
@Mock private lateinit var recProgressBar2: SeekBar
@Mock private lateinit var recProgressBar3: SeekBar
private var shouldShowBroadcastButton: Boolean = false
private val fakeFeatureFlag =
FakeFeatureFlags().apply {
@@ -636,10 +633,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindAlbumView_setAfterExecutors() {
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(Color.RED)
val albumArt = Icon.createWithBitmap(bmp)
val albumArt = getColorIcon(Color.RED)
val state = mediaData.copy(artwork = albumArt)
player.attachPlayer(viewHolder)
@@ -652,15 +646,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindAlbumView_bitmapInLaterStates_setAfterExecutors() {
val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val redCanvas = Canvas(redBmp)
redCanvas.drawColor(Color.RED)
val redArt = Icon.createWithBitmap(redBmp)
val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val greenCanvas = Canvas(greenBmp)
greenCanvas.drawColor(Color.GREEN)
val greenArt = Icon.createWithBitmap(greenBmp)
val redArt = getColorIcon(Color.RED)
val greenArt = getColorIcon(Color.GREEN)
val state0 = mediaData.copy(artwork = null)
val state1 = mediaData.copy(artwork = redArt)
@@ -705,18 +692,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun addTwoPlayerGradients_differentStates() {
// Setup redArtwork and its color scheme.
val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val redCanvas = Canvas(redBmp)
redCanvas.drawColor(Color.RED)
val redArt = Icon.createWithBitmap(redBmp)
val redArt = getColorIcon(Color.RED)
val redWallpaperColor = player.getWallpaperColor(redArt)
val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT)
// Setup greenArt and its color scheme.
val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val greenCanvas = Canvas(greenBmp)
greenCanvas.drawColor(Color.GREEN)
val greenArt = Icon.createWithBitmap(greenBmp)
val greenArt = getColorIcon(Color.GREEN)
val greenWallpaperColor = player.getWallpaperColor(greenArt)
val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT)
@@ -2040,12 +2021,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("")
.setSubtitle("subtitle2")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "title3")
.setSubtitle("subtitle3")
.setSubtitle("")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
@@ -2125,26 +2106,18 @@ public class MediaControlPanelTest : SysuiTestCase() {
assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
}
@Test
fun bindRecommendation_setAfterExecutors() {
fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
whenever(recommendationViewHolder.mediaAppIcons)
.thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
whenever(recommendationViewHolder.mediaCoverItems)
.thenReturn(listOf(coverItem, coverItem, coverItem))
whenever(recommendationViewHolder.mediaProgressBars)
.thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
whenever(recommendationViewHolder.mediaSubtitles)
.thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
whenever(coverItem.imageMatrix).thenReturn(matrix)
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(Color.RED)
val albumArt = Icon.createWithBitmap(bmp)
setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val data =
smartspaceData.copy(
recommendations =
@@ -2180,21 +2153,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindRecommendationWithProgressBars() {
fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
whenever(recommendationViewHolder.mediaAppIcons)
.thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
whenever(recommendationViewHolder.mediaCoverItems)
.thenReturn(listOf(coverItem, coverItem, coverItem))
whenever(recommendationViewHolder.mediaProgressBars)
.thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
whenever(recommendationViewHolder.mediaSubtitles)
.thenReturn(listOf(recSubtitleMock1, recSubtitleMock2, recSubtitleMock3))
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(Color.RED)
val albumArt = Icon.createWithBitmap(bmp)
useRealConstraintSets()
setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val bundle =
Bundle().apply {
putInt(
@@ -2232,26 +2193,61 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(recProgressBar1).visibility = View.VISIBLE
verify(recProgressBar2).visibility = View.GONE
verify(recProgressBar3).visibility = View.GONE
verify(recSubtitleMock1).visibility = View.GONE
verify(recSubtitleMock2).visibility = View.VISIBLE
verify(recSubtitleMock3).visibility = View.VISIBLE
assertThat(recSubtitle1.visibility).isEqualTo(View.GONE)
assertThat(recSubtitle2.visibility).isEqualTo(View.VISIBLE)
assertThat(recSubtitle3.visibility).isEqualTo(View.VISIBLE)
}
@Test
fun bindRecommendation_carouselNotFitThreeRecs() {
useRealConstraintSets()
setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("subtitle1")
.setIcon(albumArt)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("subtitle1")
.setIcon(albumArt)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "title3")
.setSubtitle("subtitle1")
.setIcon(albumArt)
.setExtras(Bundle.EMPTY)
.build()
)
)
// set the screen width less than the width of media controls.
player.context.resources.configuration.screenWidthDp = 350
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(data)
assertThat(player.numberOfFittedRecommendations).isEqualTo(2)
assertThat(expandedSet.getVisibility(coverContainer1.id)).isEqualTo(ConstraintSet.VISIBLE)
assertThat(collapsedSet.getVisibility(coverContainer1.id)).isEqualTo(ConstraintSet.VISIBLE)
assertThat(expandedSet.getVisibility(coverContainer2.id)).isEqualTo(ConstraintSet.VISIBLE)
assertThat(collapsedSet.getVisibility(coverContainer2.id)).isEqualTo(ConstraintSet.VISIBLE)
assertThat(expandedSet.getVisibility(coverContainer3.id)).isEqualTo(ConstraintSet.GONE)
assertThat(collapsedSet.getVisibility(coverContainer3.id)).isEqualTo(ConstraintSet.GONE)
}
@Test
fun addTwoRecommendationGradients_differentStates() {
// Setup redArtwork and its color scheme.
val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val redCanvas = Canvas(redBmp)
redCanvas.drawColor(Color.RED)
val redArt = Icon.createWithBitmap(redBmp)
val redArt = getColorIcon(Color.RED)
val redWallpaperColor = player.getWallpaperColor(redArt)
val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT)
// Setup greenArt and its color scheme.
val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val greenCanvas = Canvas(greenBmp)
greenCanvas.drawColor(Color.GREEN)
val greenArt = Icon.createWithBitmap(greenBmp)
val greenArt = getColorIcon(Color.GREEN)
val greenWallpaperColor = player.getWallpaperColor(greenArt)
val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT)
@@ -2392,6 +2388,34 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent))
}
private fun setupUpdatedRecommendationViewHolder() {
fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
whenever(recommendationViewHolder.mediaAppIcons)
.thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
whenever(recommendationViewHolder.mediaCoverContainers)
.thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
whenever(recommendationViewHolder.mediaCoverItems)
.thenReturn(listOf(coverItem, coverItem, coverItem))
whenever(recommendationViewHolder.mediaProgressBars)
.thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
whenever(recommendationViewHolder.mediaSubtitles)
.thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
whenever(coverItem.imageMatrix).thenReturn(matrix)
// set ids for recommendation containers
whenever(coverContainer1.id).thenReturn(1)
whenever(coverContainer2.id).thenReturn(2)
whenever(coverContainer3.id).thenReturn(3)
}
private fun getColorIcon(color: Int): Icon {
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(color)
return Icon.createWithBitmap(bmp)
}
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor {
verify(seekBarViewModel).setScrubbingChangeListener(capture())