diff options
7 files changed, 295 insertions, 26 deletions
diff --git a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml index 3c3bc63906f0..6611c59651fc 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 c7d41d3b28d3..80582efec6a4 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 fc127cc08021..827631b7eb53 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 c9d76870ebbd..a6113473b065 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 d3e2fc8f2424..571723cea616 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 @@ 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++; } diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt index b2acc92fb30d..a83984036f60 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 @@ 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) 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 607a873a73d1..df9da0ea4f37 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 @@ 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()) } } |