[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()) }
 }