[Media Recs] Add titles and subtitles to the expanded layout.
Bug: 223603970
Test: manual (see bug for screenshot)
Test: atest MediaControlPanelTest
Change-Id: Iba43aee9405a60d78d00c8945860547713adf274
diff --git a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
index 3c3bc63..6611c59 100644
--- a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
+++ b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
@@ -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"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c7d41d3..80582ef 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -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 -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index fc127cc..827631b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -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" />
diff --git a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml b/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
index c9d7687..a6113473 100644
--- a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
@@ -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>
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index d3e2fc8..571723c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -1054,6 +1054,24 @@
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++;
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
index b2acc92..a839840 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
@@ -34,11 +34,23 @@
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)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 607a873..df9da0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -171,8 +171,18 @@
@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 @@
* 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 @@
// 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 @@
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 @@
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 @@
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 @@
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()) }
}