[Media Recs] Add titles and subtitles to the expanded layout.

Bug: 223603970
Test: manual (see bug for screenshot)
Test: atest MediaControlPanelTest
Change-Id: Iba43aee9405a60d78d00c8945860547713adf274
This commit is contained in:
Caitlin Cassidy
2022-04-11 22:11:20 +00:00
parent d6090a038a
commit f6453bc32f
7 changed files with 295 additions and 26 deletions

View File

@@ -42,10 +42,10 @@
android:id="@+id/media_cover1_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/media_title1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/media_cover2_container"
android:layout_marginEnd="@dimen/qs_media_rec_album_margin"
android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintVertical_bias="0.5"
@@ -63,16 +63,33 @@
android:scaleType="centerCrop"/>
</FrameLayout>
<!-- TODO(b/223603970): Add title and subtitle below each album cover. -->
<TextView
android:id="@+id/media_title1"
style="@style/MediaPlayer.Recommendation.Text.Title"
app:layout_constraintStart_toStartOf="@+id/media_cover1_container"
app:layout_constraintEnd_toEndOf="@+id/media_cover1_container"
app:layout_constraintTop_toBottomOf="@+id/media_cover1_container"
app:layout_constraintBottom_toTopOf="@+id/media_subtitle1"
/>
<TextView
android:id="@+id/media_subtitle1"
style="@style/MediaPlayer.Recommendation.Text.Subtitle"
app:layout_constraintStart_toStartOf="@+id/media_cover1_container"
app:layout_constraintEnd_toEndOf="@+id/media_cover1_container"
app:layout_constraintTop_toBottomOf="@+id/media_title1"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="@dimen/qs_media_padding"
/>
<FrameLayout
android:id="@+id/media_cover2_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/media_title2"
app:layout_constraintStart_toEndOf="@id/media_cover1_container"
app:layout_constraintEnd_toStartOf="@id/media_cover3_container"
android:layout_marginEnd="@dimen/qs_media_rec_album_margin"
android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
app:layout_constraintVertical_bias="0.5"
>
<ImageView
@@ -86,11 +103,30 @@
android:scaleType="centerCrop"/>
</FrameLayout>
<TextView
android:id="@+id/media_title2"
style="@style/MediaPlayer.Recommendation.Text.Title"
app:layout_constraintStart_toStartOf="@+id/media_cover2_container"
app:layout_constraintEnd_toEndOf="@+id/media_cover2_container"
app:layout_constraintTop_toBottomOf="@+id/media_cover2_container"
app:layout_constraintBottom_toTopOf="@+id/media_subtitle2"
/>
<TextView
android:id="@+id/media_subtitle2"
style="@style/MediaPlayer.Recommendation.Text.Subtitle"
app:layout_constraintStart_toStartOf="@+id/media_cover2_container"
app:layout_constraintEnd_toEndOf="@+id/media_cover2_container"
app:layout_constraintTop_toBottomOf="@+id/media_title2"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="@dimen/qs_media_padding"
/>
<FrameLayout
android:id="@+id/media_cover3_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/media_title3"
app:layout_constraintStart_toEndOf="@id/media_cover2_container"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="@dimen/qs_media_padding"
@@ -107,6 +143,25 @@
android:scaleType="centerCrop"/>
</FrameLayout>
<TextView
android:id="@+id/media_title3"
style="@style/MediaPlayer.Recommendation.Text.Title"
app:layout_constraintStart_toStartOf="@+id/media_cover3_container"
app:layout_constraintEnd_toEndOf="@+id/media_cover3_container"
app:layout_constraintTop_toBottomOf="@+id/media_cover3_container"
app:layout_constraintBottom_toTopOf="@+id/media_subtitle3"
/>
<TextView
android:id="@+id/media_subtitle3"
style="@style/MediaPlayer.Recommendation.Text.Subtitle"
app:layout_constraintStart_toStartOf="@+id/media_cover3_container"
app:layout_constraintEnd_toEndOf="@+id/media_cover3_container"
app:layout_constraintTop_toBottomOf="@+id/media_title3"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="@dimen/qs_media_padding"
/>
<!-- Long press menu -->
<TextView
android:layout_width="match_parent"

View File

@@ -980,7 +980,8 @@
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
<dimen name="qs_media_rec_album_size">88dp</dimen>
<dimen name="qs_media_rec_album_margin">16dp</dimen>
<dimen name="qs_media_rec_album_side_margin">16dp</dimen>
<dimen name="qs_media_rec_album_bottom_margin">8dp</dimen>
<dimen name="qs_media_rec_icon_size">24dp</dimen>
<!-- Media tap-to-transfer chip for sender device -->

View File

@@ -672,13 +672,31 @@
<item name="android:layout_height">@dimen/qs_media_rec_album_size</item>
<item name="android:background">@drawable/qs_media_light_source</item>
<item name="android:layout_marginTop">@dimen/qs_media_padding</item>
<item name="android:layout_marginBottom">@dimen/qs_media_padding</item>
<item name="android:layout_marginBottom">@dimen/qs_media_rec_album_bottom_margin</item>
</style>
<style name="MediaPlayer.Recommendation.Album">
<item name="android:backgroundTint">@color/media_player_album_bg</item>
</style>
<style name="MediaPlayer.Recommendation.Text">
<item name="android:layout_width">@dimen/qs_media_rec_album_size</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
<item name="android:textSize">14sp</item>
<item name="android:gravity">start</item>
</style>
<style name="MediaPlayer.Recommendation.Text.Title">
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="MediaPlayer.Recommendation.Text.Subtitle">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<!-- Used to style charging animation AVD animation -->
<style name="ChargingAnim" />

View File

@@ -15,7 +15,8 @@
~ limitations under the License
-->
<ConstraintSet
xmlns:android="http://schemas.android.com/apk/res/android" >
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<Constraint
android:id="@+id/sizing_view"
@@ -23,4 +24,76 @@
android:layout_height="@dimen/qs_media_session_height_collapsed"
/>
<!-- Only the constraintBottom and marginBottom are different. The rest of the constraints are
the same as the constraints in media_smartspace_recommendations. But due to how
ConstraintSets work, all the constraints need to be in the same place.
Ditto for the other cover containers. -->
<Constraint
android:id="@+id/media_cover1_container"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="@dimen/qs_media_padding"
style="@style/MediaPlayer.Recommendation.AlbumContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/media_cover2_container"
android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintVertical_bias="0.5"
/>
<Constraint
android:id="@+id/media_title1"
android:visibility="gone"
/>
<Constraint
android:id="@+id/media_subtitle1"
android:visibility="gone"
/>
<Constraint
android:id="@+id/media_cover2_container"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="@dimen/qs_media_padding"
style="@style/MediaPlayer.Recommendation.AlbumContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/media_cover1_container"
app:layout_constraintEnd_toStartOf="@id/media_cover3_container"
android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
app:layout_constraintVertical_bias="0.5"
/>
<Constraint
android:id="@+id/media_title2"
android:visibility="gone"
/>
<Constraint
android:id="@+id/media_subtitle2"
android:visibility="gone"
/>
<Constraint
android:id="@+id/media_cover3_container"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="@dimen/qs_media_padding"
style="@style/MediaPlayer.Recommendation.AlbumContainer"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/media_cover2_container"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="@dimen/qs_media_padding"
app:layout_constraintVertical_bias="0.5"
/>
<Constraint
android:id="@+id/media_title3"
android:visibility="gone"
/>
<Constraint
android:id="@+id/media_subtitle3"
android:visibility="gone"
/>
</ConstraintSet>

View File

@@ -1054,6 +1054,24 @@ public class MediaControlPanel {
recommendation.getTitle(), artistName, appName));
}
// Set up title
CharSequence title = recommendation.getTitle();
TextView titleView =
mRecommendationViewHolder.getMediaTitles().get(uiComponentIndex);
titleView.setText(title);
// TODO(b/223603970): If none of them have titles, should we then hide the views?
// Set up subtitle
CharSequence subtitle = recommendation.getSubtitle();
TextView subtitleView =
mRecommendationViewHolder.getMediaSubtitles().get(uiComponentIndex);
// It would look awkward to show a subtitle if we don't have a title.
boolean shouldShowSubtitleText = !TextUtils.isEmpty(title);
CharSequence subtitleText = shouldShowSubtitleText ? subtitle : "";
subtitleView.setText(subtitleText);
// TODO(b/223603970): If none of them have subtitles, should we then hide the views?
uiComponentIndex++;
}

View File

@@ -34,11 +34,23 @@ class RecommendationViewHolder private constructor(itemView: View) {
val mediaCoverItems = listOf<ImageView>(
itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover2),
itemView.requireViewById(R.id.media_cover3))
itemView.requireViewById(R.id.media_cover3)
)
val mediaCoverContainers = listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover1_container),
itemView.requireViewById(R.id.media_cover2_container),
itemView.requireViewById(R.id.media_cover3_container))
itemView.requireViewById(R.id.media_cover3_container)
)
val mediaTitles: List<TextView> = listOf(
itemView.requireViewById(R.id.media_title1),
itemView.requireViewById(R.id.media_title2),
itemView.requireViewById(R.id.media_title3)
)
val mediaSubtitles: List<TextView> = listOf(
itemView.requireViewById(R.id.media_subtitle1),
itemView.requireViewById(R.id.media_subtitle2),
itemView.requireViewById(R.id.media_subtitle3)
)
// Settings/Guts screen
val longPressText = itemView.requireViewById<TextView>(R.id.remove_text)

View File

@@ -171,8 +171,18 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var recommendationViewHolder: RecommendationViewHolder
@Mock private lateinit var smartspaceAction: SmartspaceAction
private lateinit var smartspaceData: SmartspaceMediaData
@Mock private lateinit var coverContainer: ViewGroup
private lateinit var coverItem: ImageView
@Mock private lateinit var coverContainer1: ViewGroup
@Mock private lateinit var coverContainer2: ViewGroup
@Mock private lateinit var coverContainer3: ViewGroup
private lateinit var coverItem1: ImageView
private lateinit var coverItem2: ImageView
private lateinit var coverItem3: ImageView
private lateinit var recTitle1: TextView
private lateinit var recTitle2: TextView
private lateinit var recTitle3: TextView
private lateinit var recSubtitle1: TextView
private lateinit var recSubtitle2: TextView
private lateinit var recSubtitle3: TextView
@JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -371,14 +381,30 @@ public class MediaControlPanelTest : SysuiTestCase() {
* Initialize elements for the recommendation view holder
*/
private fun initRecommendationViewHolderMocks() {
recTitle1 = TextView(context)
recTitle2 = TextView(context)
recTitle3 = TextView(context)
recSubtitle1 = TextView(context)
recSubtitle2 = TextView(context)
recSubtitle3 = TextView(context)
whenever(recommendationViewHolder.recommendations).thenReturn(view)
whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon)
// Add a recommendation item
coverItem = ImageView(context).also { it.setId(R.id.media_cover1) }
whenever(coverContainer.id).thenReturn(R.id.media_cover1_container)
whenever(recommendationViewHolder.mediaCoverItems).thenReturn(listOf(coverItem))
whenever(recommendationViewHolder.mediaCoverContainers).thenReturn(listOf(coverContainer))
coverItem1 = ImageView(context).also { it.setId(R.id.media_cover1) }
coverItem2 = ImageView(context).also { it.setId(R.id.media_cover2) }
coverItem3 = ImageView(context).also { it.setId(R.id.media_cover3) }
whenever(recommendationViewHolder.mediaCoverItems)
.thenReturn(listOf(coverItem1, coverItem2, coverItem3))
whenever(recommendationViewHolder.mediaCoverContainers)
.thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
whenever(recommendationViewHolder.mediaTitles)
.thenReturn(listOf(recTitle1, recTitle2, recTitle3))
whenever(recommendationViewHolder.mediaSubtitles).thenReturn(
listOf(recSubtitle1, recSubtitle2, recSubtitle3)
)
// Long press menu
whenever(recommendationViewHolder.settings).thenReturn(settings)
@@ -391,7 +417,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Needed for card and item action click
val mockContext = mock(Context::class.java)
whenever(view.context).thenReturn(mockContext)
whenever(coverContainer.context).thenReturn(mockContext)
whenever(coverContainer1.context).thenReturn(mockContext)
whenever(coverContainer2.context).thenReturn(mockContext)
whenever(coverContainer3.context).thenReturn(mockContext)
}
@After
@@ -673,9 +702,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
MediaAction(icon, null, "custom 0", bg),
MediaAction(icon, Runnable {}, "custom 1", bg)
)
val state = mediaData.copy(actions = actions,
val state = mediaData.copy(
actions = actions,
actionsToShowInCompact = listOf(1, 2),
semanticActions = null)
semanticActions = null
)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
@@ -724,11 +755,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
val icon = context.getDrawable(R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.ic_media_play_container)
val semanticActions0 = MediaButton(
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
)
val semanticActions1 = MediaButton(
playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null))
playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)
)
val semanticActions2 = MediaButton(
playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null))
playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)
)
val state0 = mediaData.copy(semanticActions = semanticActions0)
val state1 = mediaData.copy(semanticActions = semanticActions1)
val state2 = mediaData.copy(semanticActions = semanticActions2)
@@ -768,8 +802,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
assertThat(actionPlayPause.getBackground()).isNull()
verify(mockAvd0, times(1))
.registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
verify(mockAvd1, times(1)
).registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
verify(mockAvd1, times(1))
.registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
verify(mockAvd2, times(1))
.registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
verify(mockAvd0, times(1))
@@ -1219,12 +1253,70 @@ public class MediaControlPanelTest : SysuiTestCase() {
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(smartspaceData)
verify(coverContainer).setOnClickListener(captor.capture())
verify(coverContainer1).setOnClickListener(captor.capture())
captor.value.onClick(recommendationViewHolder.recommendations)
verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0))
}
@Test
fun bindRecommendation_hasTitlesAndSubtitles() {
player.attachRecommendation(recommendationViewHolder)
val title1 = "Title1"
val title2 = "Title2"
val title3 = "Title3"
val subtitle1 = "Subtitle1"
val subtitle2 = "Subtitle2"
val subtitle3 = "Subtitle3"
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", title1)
.setSubtitle(subtitle1)
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", title2)
.setSubtitle(subtitle2)
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", title3)
.setSubtitle(subtitle3)
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
)
player.bindRecommendation(data)
assertThat(recTitle1.text).isEqualTo(title1)
assertThat(recTitle2.text).isEqualTo(title2)
assertThat(recTitle3.text).isEqualTo(title3)
assertThat(recSubtitle1.text).isEqualTo(subtitle1)
assertThat(recSubtitle2.text).isEqualTo(subtitle2)
assertThat(recSubtitle3.text).isEqualTo(subtitle3)
}
@Test
fun bindRecommendation_noTitle_subtitleNotShown() {
player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("fake subtitle")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
)
player.bindRecommendation(data)
assertThat(recSubtitle1.text).isEqualTo("")
}
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
}