Merge "Update media recommendation card layout" into tm-qpr-dev am: 121599e660

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21092056

Change-Id: I206bb3e7d9aa19ea2afb94782c3e633e5fb0c7b2
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Michael Mikhail
2023-01-30 15:37:20 +00:00
committed by Automerger Merge Worker
16 changed files with 564 additions and 67 deletions

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<!-- Layout for media recommendation item inside QSPanel carousel -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Album cover -->
<ImageView
android:id="@+id/media_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:translationZ="0dp"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:clipToOutline="true"
android:background="@drawable/bg_smartspace_media_item"/>
<!-- App icon -->
<com.android.internal.widget.CachingIconView
android:id="@+id/media_rec_app_icon"
android:layout_width="@dimen/qs_media_rec_icon_top_margin"
android:layout_height="@dimen/qs_media_rec_icon_top_margin"
android:layout_marginStart="@dimen/qs_media_info_spacing"
android:layout_marginTop="@dimen/qs_media_info_spacing"/>
<!-- Artist name -->
<TextView
android:id="@+id/media_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/qs_media_info_spacing"
android:layout_marginEnd="@dimen/qs_media_info_spacing"
android:layout_marginBottom="@dimen/qs_media_rec_album_title_bottom_margin"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:singleLine="true"
android:textSize="12sp"
android:gravity="top"
android:layout_gravity="bottom"/>
<!-- Album name -->
<TextView
android:id="@+id/media_subtitle"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_rec_album_subtitle_height"
android:layout_marginEnd="@dimen/qs_media_info_spacing"
android:layout_marginStart="@dimen/qs_media_info_spacing"
android:layout_marginBottom="@dimen/qs_media_info_spacing"
android:fontFamily="@*android:string/config_headlineFontFamily"
android:singleLine="true"
android:textSize="11sp"
android:gravity="center_vertical"
android:layout_gravity="bottom"/>
</merge>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<!-- Layout for media recommendations inside QSPanel carousel -->
<com.android.systemui.util.animation.TransitionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_recommendations_updated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:forceHasOverlappingRendering="false"
android:background="@drawable/qs_media_background"
android:theme="@style/MediaPlayer">
<!-- This view just ensures the full media player is a certain height. -->
<View
android:id="@+id/sizing_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_session_height_expanded" />
<TextView
android:id="@+id/media_rec_title"
style="@style/MediaPlayer.Recommendation.Header"
android:text="@string/controls_media_smartspace_rec_header"/>
<FrameLayout
android:id="@+id/media_cover1_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
>
<include
layout="@layout/media_recommendation_view"/>
</FrameLayout>
<FrameLayout
android:id="@+id/media_cover2_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
>
<include
layout="@layout/media_recommendation_view"/>
</FrameLayout>
<FrameLayout
android:id="@+id/media_cover3_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
>
<include
layout="@layout/media_recommendation_view"/>
</FrameLayout>
<include
layout="@layout/media_long_press_menu" />
</com.android.systemui.util.animation.TransitionLayout>

View File

@@ -1068,8 +1068,13 @@
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> <!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
<dimen name="qs_media_rec_icon_top_margin">16dp</dimen> <dimen name="qs_media_rec_icon_top_margin">16dp</dimen>
<dimen name="qs_media_rec_album_size">88dp</dimen> <dimen name="qs_media_rec_album_size">88dp</dimen>
<dimen name="qs_media_rec_album_width">110dp</dimen>
<dimen name="qs_media_rec_album_height_expanded">108dp</dimen>
<dimen name="qs_media_rec_album_height_collapsed">77dp</dimen>
<dimen name="qs_media_rec_album_side_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_album_bottom_margin">8dp</dimen>
<dimen name="qs_media_rec_album_title_bottom_margin">22dp</dimen>
<dimen name="qs_media_rec_album_subtitle_height">12dp</dimen>
<!-- Media tap-to-transfer chip for sender device --> <!-- Media tap-to-transfer chip for sender device -->
<dimen name="media_ttt_chip_outer_padding">16dp</dimen> <dimen name="media_ttt_chip_outer_padding">16dp</dimen>

View File

@@ -2360,6 +2360,8 @@
<string name="controls_media_smartspace_rec_item_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string> <string name="controls_media_smartspace_rec_item_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
<!-- Description for Smartspace recommendation's media item which doesn't have artist info, including information for the media's title and the source app [CHAR LIMIT=NONE]--> <!-- Description for Smartspace recommendation's media item which doesn't have artist info, including information for the media's title and the source app [CHAR LIMIT=NONE]-->
<string name="controls_media_smartspace_rec_item_no_artist_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%2$s</xliff:g></string> <string name="controls_media_smartspace_rec_item_no_artist_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%2$s</xliff:g></string>
<!-- Header title for Smartspace recommendation card within media controls. [CHAR_LIMIT=30] -->
<string name="controls_media_smartspace_rec_header">For You</string>
<!--- ****** Media tap-to-transfer ****** --> <!--- ****** Media tap-to-transfer ****** -->
<!-- Text for a button to undo the media transfer. [CHAR LIMIT=20] --> <!-- Text for a button to undo the media transfer. [CHAR LIMIT=20] -->

View File

@@ -678,6 +678,17 @@
<style name="MediaPlayer.Recommendation"/> <style name="MediaPlayer.Recommendation"/>
<style name="MediaPlayer.Recommendation.Header">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">@dimen/qs_media_padding</item>
<item name="android:layout_marginStart">@dimen/qs_media_padding</item>
<item name="android:fontFamily">=@*android:string/config_headlineFontFamilyMedium</item>
<item name="android:singleLine">true</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
<style name="MediaPlayer.Recommendation.AlbumContainer"> <style name="MediaPlayer.Recommendation.AlbumContainer">
<item name="android:layout_width">@dimen/qs_media_rec_album_size</item> <item name="android:layout_width">@dimen/qs_media_rec_album_size</item>
<item name="android:layout_height">@dimen/qs_media_rec_album_size</item> <item name="android:layout_height">@dimen/qs_media_rec_album_size</item>
@@ -686,6 +697,12 @@
<item name="android:layout_marginBottom">@dimen/qs_media_rec_album_bottom_margin</item> <item name="android:layout_marginBottom">@dimen/qs_media_rec_album_bottom_margin</item>
</style> </style>
<style name="MediaPlayer.Recommendation.AlbumContainer.Updated">
<item name="android:layout_width">@dimen/qs_media_rec_album_width</item>
<item name="android:background">@drawable/qs_media_light_source</item>
<item name="android:layout_marginTop">@dimen/qs_media_info_spacing</item>
</style>
<style name="MediaPlayer.Recommendation.Album"> <style name="MediaPlayer.Recommendation.Album">
<item name="android:backgroundTint">@color/media_player_album_bg</item> <item name="android:backgroundTint">@color/media_player_album_bg</item>
</style> </style>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<ConstraintSet
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<Constraint
android:id="@+id/sizing_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_session_height_collapsed"
/>
<Constraint
android:id="@+id/media_rec_title"
style="@style/MediaPlayer.Recommendation.Header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Constraint
android:id="@+id/media_cover1_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
android:layout_marginEnd="@dimen/qs_media_info_spacing"
android:layout_marginStart="@dimen/qs_media_padding"
app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/>
<Constraint
android:id="@+id/media_cover2_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
android:layout_marginEnd="@dimen/qs_media_info_spacing"
app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
app:layout_constraintStart_toEndOf="@id/media_cover1_container"
app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/>
<Constraint
android:id="@+id/media_cover3_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
android:layout_height="@dimen/qs_media_rec_album_height_collapsed"
android:layout_marginEnd="@dimen/qs_media_padding"
app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
app:layout_constraintStart_toEndOf="@id/media_cover2_container"
app:layout_constraintEnd_toEndOf="parent"/>
</ConstraintSet>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<ConstraintSet
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<Constraint
android:id="@+id/sizing_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_session_height_expanded"
/>
<Constraint
android:id="@+id/media_rec_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/qs_media_padding"
android:layout_marginStart="@dimen/qs_media_padding"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
android:singleLine="true"
android:textSize="14sp"
android:textColor="@color/notification_primary_text_color"/>
<Constraint
android:id="@+id/media_cover1_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
android:layout_height="@dimen/qs_media_rec_album_height_expanded"
android:layout_marginEnd="@dimen/qs_media_info_spacing"
android:layout_marginStart="@dimen/qs_media_padding"
app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/>
<Constraint
android:id="@+id/media_cover2_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
android:layout_height="@dimen/qs_media_rec_album_height_expanded"
android:layout_marginEnd="@dimen/qs_media_info_spacing"
app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
app:layout_constraintStart_toEndOf="@id/media_cover1_container"
app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/>
<Constraint
android:id="@+id/media_cover3_container"
style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated"
android:layout_height="@dimen/qs_media_rec_album_height_expanded"
android:layout_marginEnd="@dimen/qs_media_padding"
app:layout_constraintTop_toBottomOf="@+id/media_rec_title"
app:layout_constraintStart_toEndOf="@id/media_cover2_container"
app:layout_constraintEnd_toEndOf="parent"/>
</ConstraintSet>

View File

@@ -365,6 +365,10 @@ object Flags {
// TODO(b/266157412): Tracking Bug // TODO(b/266157412): Tracking Bug
val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions") val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
// TODO(b/266739309): Tracking Bug
@JvmField
val MEDIA_RECOMMENDATION_CARD_UPDATE = unreleasedFlag(914, "media_recommendation_card_update")
// 1000 - dock // 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -21,6 +21,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.android.internal.widget.CachingIconView
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.ui.IlluminationDrawable import com.android.systemui.media.controls.ui.IlluminationDrawable
@@ -29,18 +30,15 @@ import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "RecommendationViewHolder" private const val TAG = "RecommendationViewHolder"
/** ViewHolder for a Smartspace media recommendation. */ /** ViewHolder for a Smartspace media recommendation. */
class RecommendationViewHolder private constructor(itemView: View) { class RecommendationViewHolder private constructor(itemView: View, updatedView: Boolean) {
val recommendations = itemView as TransitionLayout val recommendations = itemView as TransitionLayout
// Recommendation screen // Recommendation screen
val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon) lateinit var cardIcon: ImageView
val mediaCoverItems = lateinit var mediaAppIcons: List<CachingIconView>
listOf<ImageView>( lateinit var cardTitle: TextView
itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover2),
itemView.requireViewById(R.id.media_cover3)
)
val mediaCoverContainers = val mediaCoverContainers =
listOf<ViewGroup>( listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover1_container), itemView.requireViewById(R.id.media_cover1_container),
@@ -48,21 +46,45 @@ class RecommendationViewHolder private constructor(itemView: View) {
itemView.requireViewById(R.id.media_cover3_container) itemView.requireViewById(R.id.media_cover3_container)
) )
val mediaTitles: List<TextView> = val mediaTitles: List<TextView> =
listOf( if (updatedView) {
itemView.requireViewById(R.id.media_title1), mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
itemView.requireViewById(R.id.media_title2), } else {
itemView.requireViewById(R.id.media_title3) listOf(
) itemView.requireViewById(R.id.media_title1),
itemView.requireViewById(R.id.media_title2),
itemView.requireViewById(R.id.media_title3)
)
}
val mediaSubtitles: List<TextView> = val mediaSubtitles: List<TextView> =
listOf( if (updatedView) {
itemView.requireViewById(R.id.media_subtitle1), mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
itemView.requireViewById(R.id.media_subtitle2), } else {
itemView.requireViewById(R.id.media_subtitle3) listOf(
) itemView.requireViewById(R.id.media_subtitle1),
itemView.requireViewById(R.id.media_subtitle2),
itemView.requireViewById(R.id.media_subtitle3)
)
}
val mediaCoverItems: List<ImageView> =
if (updatedView) {
mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
} else {
listOf(
itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover2),
itemView.requireViewById(R.id.media_cover3)
)
}
val gutsViewHolder = GutsViewHolder(itemView) val gutsViewHolder = GutsViewHolder(itemView)
init { init {
if (updatedView) {
mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
cardTitle = itemView.requireViewById(R.id.media_rec_title)
} else {
cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
}
(recommendations.background as IlluminationDrawable).let { background -> (recommendations.background as IlluminationDrawable).let { background ->
mediaCoverContainers.forEach { background.registerLightSource(it) } mediaCoverContainers.forEach { background.registerLightSource(it) }
background.registerLightSource(gutsViewHolder.cancel) background.registerLightSource(gutsViewHolder.cancel)
@@ -83,36 +105,52 @@ class RecommendationViewHolder private constructor(itemView: View) {
* @param parent Parent of inflated view. * @param parent Parent of inflated view.
*/ */
@JvmStatic @JvmStatic
fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder { fun create(
inflater: LayoutInflater,
parent: ViewGroup,
updatedView: Boolean,
): RecommendationViewHolder {
val itemView = val itemView =
inflater.inflate( if (updatedView) {
R.layout.media_smartspace_recommendations, inflater.inflate(
parent, R.layout.media_recommendations,
false /* attachToRoot */ parent,
) false /* attachToRoot */
)
} else {
inflater.inflate(
R.layout.media_smartspace_recommendations,
parent,
false /* attachToRoot */
)
}
// Because this media view (a TransitionLayout) is used to measure and layout the views // Because this media view (a TransitionLayout) is used to measure and layout the views
// in various states before being attached to its parent, we can't depend on the default // in various states before being attached to its parent, we can't depend on the default
// LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
return RecommendationViewHolder(itemView) return RecommendationViewHolder(itemView, updatedView)
} }
// Res Ids for the control components on the recommendation view. // Res Ids for the control components on the recommendation view.
val controlsIds = val controlsIds =
setOf( setOf(
R.id.recommendation_card_icon, R.id.recommendation_card_icon,
R.id.media_rec_title,
R.id.media_cover1, R.id.media_cover1,
R.id.media_cover2, R.id.media_cover2,
R.id.media_cover3, R.id.media_cover3,
R.id.media_cover,
R.id.media_cover1_container, R.id.media_cover1_container,
R.id.media_cover2_container, R.id.media_cover2_container,
R.id.media_cover3_container, R.id.media_cover3_container,
R.id.media_title1, R.id.media_title1,
R.id.media_title2, R.id.media_title2,
R.id.media_title3, R.id.media_title3,
R.id.media_title,
R.id.media_subtitle1, R.id.media_subtitle1,
R.id.media_subtitle2, R.id.media_subtitle2,
R.id.media_subtitle3 R.id.media_subtitle3,
R.id.media_subtitle,
) )
val mediaTitlesAndSubtitlesIds = val mediaTitlesAndSubtitlesIds =
@@ -120,9 +158,11 @@ class RecommendationViewHolder private constructor(itemView: View) {
R.id.media_title1, R.id.media_title1,
R.id.media_title2, R.id.media_title2,
R.id.media_title3, R.id.media_title3,
R.id.media_title,
R.id.media_subtitle1, R.id.media_subtitle1,
R.id.media_subtitle2, R.id.media_subtitle2,
R.id.media_subtitle3 R.id.media_subtitle3,
R.id.media_subtitle,
) )
val mediaContainersIds = val mediaContainersIds =

View File

@@ -43,6 +43,7 @@ import com.android.systemui.media.controls.models.recommendation.RecommendationV
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.util.SmallHash import com.android.systemui.media.controls.util.SmallHash
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
@@ -88,7 +89,8 @@ constructor(
falsingManager: FalsingManager, falsingManager: FalsingManager,
dumpManager: DumpManager, dumpManager: DumpManager,
private val logger: MediaUiEventLogger, private val logger: MediaUiEventLogger,
private val debugLogger: MediaCarouselControllerLogger private val debugLogger: MediaCarouselControllerLogger,
private val mediaFlags: MediaFlags,
) : Dumpable { ) : Dumpable {
/** The current width of the carousel */ /** The current width of the carousel */
private var currentCarouselWidth: Int = 0 private var currentCarouselWidth: Int = 0
@@ -647,7 +649,11 @@ constructor(
val newRecs = mediaControlPanelFactory.get() val newRecs = mediaControlPanelFactory.get()
newRecs.attachRecommendation( newRecs.attachRecommendation(
RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) RecommendationViewHolder.create(
LayoutInflater.from(context),
mediaContent,
mediaFlags.isRecommendationCardUpdateEnabled()
)
) )
newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
val lp = val lp =

View File

@@ -61,6 +61,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
@@ -752,43 +753,16 @@ public class MediaControlPanel {
int width = mMediaViewHolder.getAlbumView().getMeasuredWidth(); int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
int height = mMediaViewHolder.getAlbumView().getMeasuredHeight(); int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
// WallpaperColors.fromBitmap takes a good amount of time. We do that work
// on the background executor to avoid stalling animations on the UI Thread.
mBackgroundExecutor.execute(() -> { mBackgroundExecutor.execute(() -> {
// Album art // Album art
ColorScheme mutableColorScheme = null; ColorScheme mutableColorScheme = null;
Drawable artwork; Drawable artwork;
boolean isArtworkBound; boolean isArtworkBound;
Icon artworkIcon = data.getArtwork(); Icon artworkIcon = data.getArtwork();
WallpaperColors wallpaperColors = null; WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
if (artworkIcon != null) {
if (artworkIcon.getType() == Icon.TYPE_BITMAP
|| artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
// Avoids extra processing if this is already a valid bitmap
wallpaperColors = WallpaperColors
.fromBitmap(artworkIcon.getBitmap());
} else {
Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
if (artworkDrawable != null) {
wallpaperColors = WallpaperColors
.fromDrawable(artworkIcon.loadDrawable(mContext));
}
}
}
if (wallpaperColors != null) { if (wallpaperColors != null) {
mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
Drawable albumArt = getScaledBackground(artworkIcon, width, height); artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
GradientDrawable gradient = (GradientDrawable) mContext
.getDrawable(R.drawable.qs_media_scrim);
gradient.setColors(new int[] {
ColorUtilKt.getColorWithAlpha(
MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
0.25f),
ColorUtilKt.getColorWithAlpha(
MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
0.9f),
});
artwork = new LayerDrawable(new Drawable[] { albumArt, gradient });
isArtworkBound = true; isArtworkBound = true;
} else { } else {
// If there's no artwork, use colors from the app icon // If there's no artwork, use colors from the app icon
@@ -867,6 +841,96 @@ public class MediaControlPanel {
}); });
} }
private void bindRecommendationArtwork(
SmartspaceAction recommendation,
String packageName,
int itemIndex
) {
final int traceCookie = recommendation.hashCode();
final String traceName =
"MediaControlPanel#bindRecommendationArtwork<" + packageName + ">";
Trace.beginAsyncSection(traceName, traceCookie);
// Capture width & height from views in foreground for artwork scaling in background
int width = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredWidth();
int height = mRecommendationViewHolder.getMediaCoverContainers().get(0).getMeasuredHeight();
mBackgroundExecutor.execute(() -> {
// Album art
ColorScheme mutableColorScheme = null;
Drawable artwork;
Icon artworkIcon = recommendation.getIcon();
WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
if (wallpaperColors != null) {
mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
} else {
artwork = new ColorDrawable(Color.TRANSPARENT);
}
mMainExecutor.execute(() -> {
// Bind the artwork drawable to media cover.
ImageView mediaCover =
mRecommendationViewHolder.getMediaCoverItems().get(itemIndex);
mediaCover.setImageDrawable(artwork);
// Set up the app icon.
ImageView appIconView = mRecommendationViewHolder.getMediaAppIcons().get(itemIndex);
appIconView.clearColorFilter();
try {
Drawable icon = mContext.getPackageManager()
.getApplicationIcon(packageName);
appIconView.setImageDrawable(icon);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot find icon for package " + packageName, e);
appIconView.setImageResource(R.drawable.ic_music_note);
}
Trace.endAsyncSection(traceName, traceCookie);
});
});
}
// This method should be called from a background thread. WallpaperColors.fromBitmap takes a
// good amount of time. We do that work on the background executor to avoid stalling animations
// on the UI Thread.
private WallpaperColors getWallpaperColor(Icon artworkIcon) {
if (artworkIcon != null) {
if (artworkIcon.getType() == Icon.TYPE_BITMAP
|| artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
// Avoids extra processing if this is already a valid bitmap
return WallpaperColors
.fromBitmap(artworkIcon.getBitmap());
} else {
Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
if (artworkDrawable != null) {
return WallpaperColors
.fromDrawable(artworkIcon.loadDrawable(mContext));
}
}
}
return null;
}
private LayerDrawable addGradientToIcon(
Icon artworkIcon,
ColorScheme mutableColorScheme,
int width,
int height
) {
Drawable albumArt = getScaledBackground(artworkIcon, width, height);
GradientDrawable gradient = (GradientDrawable) AppCompatResources
.getDrawable(mContext, R.drawable.qs_media_scrim);
gradient.setColors(new int[] {
ColorUtilKt.getColorWithAlpha(
MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
0.25f),
ColorUtilKt.getColorWithAlpha(
MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
0.9f),
});
return new LayerDrawable(new Drawable[] { albumArt, gradient });
}
private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer, private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
int targetWidth, int targetHeight) { int targetWidth, int targetHeight) {
Drawable drawable = transitionDrawable.getDrawable(layer); Drawable drawable = transitionDrawable.getDrawable(layer);
@@ -1224,8 +1288,10 @@ public class MediaControlPanel {
PackageManager packageManager = mContext.getPackageManager(); PackageManager packageManager = mContext.getPackageManager();
// Set up media source app's logo. // Set up media source app's logo.
Drawable icon = packageManager.getApplicationIcon(applicationInfo); Drawable icon = packageManager.getApplicationIcon(applicationInfo);
ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon(); if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
headerLogoImageView.setImageDrawable(icon); ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
headerLogoImageView.setImageDrawable(icon);
}
fetchAndUpdateRecommendationColors(icon); fetchAndUpdateRecommendationColors(icon);
// Set up media rec card's tap action if applicable. // Set up media rec card's tap action if applicable.
@@ -1245,7 +1311,15 @@ public class MediaControlPanel {
// Set up media item cover. // Set up media item cover.
ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex); ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
mediaCoverImageView.setImageIcon(recommendation.getIcon()); if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
bindRecommendationArtwork(
recommendation,
data.getPackageName(),
itemIndex
);
} else {
mediaCoverImageView.setImageIcon(recommendation.getIcon());
}
// Set up the media item's click listener if applicable. // Set up the media item's click listener if applicable.
ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex); ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
@@ -1275,7 +1349,6 @@ public class MediaControlPanel {
recommendation.getTitle(), artistName, appName)); recommendation.getTitle(), artistName, appName));
} }
// Set up title // Set up title
CharSequence title = recommendation.getTitle(); CharSequence title = recommendation.getTitle();
hasTitle |= !TextUtils.isEmpty(title); hasTitle |= !TextUtils.isEmpty(title);
@@ -1353,6 +1426,10 @@ public class MediaControlPanel {
int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme); int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme); int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
}
mRecommendationViewHolder.getRecommendations() mRecommendationViewHolder.getRecommendations()
.setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
mRecommendationViewHolder.getMediaTitles().forEach( mRecommendationViewHolder.getMediaTitles().forEach(

View File

@@ -25,6 +25,7 @@ import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayout
@@ -45,7 +46,8 @@ constructor(
private val context: Context, private val context: Context,
private val configurationController: ConfigurationController, private val configurationController: ConfigurationController,
private val mediaHostStatesManager: MediaHostStatesManager, private val mediaHostStatesManager: MediaHostStatesManager,
private val logger: MediaViewLogger private val logger: MediaViewLogger,
private val mediaFlags: MediaFlags,
) { ) {
/** /**
@@ -646,8 +648,13 @@ constructor(
expandedLayout.load(context, R.xml.media_session_expanded) expandedLayout.load(context, R.xml.media_session_expanded)
} }
TYPE.RECOMMENDATION -> { TYPE.RECOMMENDATION -> {
collapsedLayout.load(context, R.xml.media_recommendation_collapsed) if (mediaFlags.isRecommendationCardUpdateEnabled()) {
expandedLayout.load(context, R.xml.media_recommendation_expanded) collapsedLayout.load(context, R.xml.media_recommendations_view_collapsed)
expandedLayout.load(context, R.xml.media_recommendations_view_expanded)
} else {
collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
expandedLayout.load(context, R.xml.media_recommendation_expanded)
}
} }
} }
refreshState() refreshState()

View File

@@ -51,4 +51,8 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
* whether the underlying notification was dismissed * whether the underlying notification was dismissed
*/ */
fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS) fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS)
/** Check whether we show the updated recommendation card. */
fun isRecommendationCardUpdateEnabled() =
featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
} }

View File

@@ -33,6 +33,7 @@ import com.android.systemui.media.controls.models.recommendation.SmartspaceMedia
import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager
@@ -87,6 +88,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Mock lateinit var smartspaceMediaData: SmartspaceMediaData @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
@Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var mediaCarousel: MediaScrollView
@Mock lateinit var pageIndicator: PageIndicator @Mock lateinit var pageIndicator: PageIndicator
@Mock lateinit var mediaFlags: MediaFlags
@Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
@Captor @Captor
lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener> lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener>
@@ -114,7 +116,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
falsingManager, falsingManager,
dumpManager, dumpManager,
logger, logger,
debugLogger debugLogger,
mediaFlags,
) )
verify(configurationController).addCallback(capture(configListener)) verify(configurationController).addCallback(capture(configListener))
verify(mediaDataManager).addListener(capture(listener)) verify(mediaDataManager).addListener(capture(listener))

View File

@@ -204,6 +204,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var coverContainer1: ViewGroup @Mock private lateinit var coverContainer1: ViewGroup
@Mock private lateinit var coverContainer2: ViewGroup @Mock private lateinit var coverContainer2: ViewGroup
@Mock private lateinit var coverContainer3: ViewGroup @Mock private lateinit var coverContainer3: ViewGroup
@Mock private lateinit var recAppIconItem: CachingIconView
@Mock private lateinit var recCardTitle: TextView
@Mock private lateinit var coverItem: ImageView
private lateinit var coverItem1: ImageView private lateinit var coverItem1: ImageView
private lateinit var coverItem2: ImageView private lateinit var coverItem2: ImageView
private lateinit var coverItem3: ImageView private lateinit var coverItem3: ImageView
@@ -220,6 +223,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
this.set(Flags.UMO_TURBULENCE_NOISE, false) this.set(Flags.UMO_TURBULENCE_NOISE, false)
this.set(Flags.MEDIA_FALSING_PENALTY, true) this.set(Flags.MEDIA_FALSING_PENALTY, true)
this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true) this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true)
this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
} }
@JvmField @Rule val mockito = MockitoJUnit.rule() @JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -2058,6 +2062,51 @@ public class MediaControlPanelTest : SysuiTestCase() {
assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) assertThat(expandedSet.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))
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(Color.RED)
val albumArt = Icon.createWithBitmap(bmp)
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()
)
)
player.attachRecommendation(recommendationViewHolder)
player.bindRecommendation(data)
bgExecutor.runAllReady()
mainExecutor.runAllReady()
verify(recCardTitle).setTextColor(any<Int>())
verify(recAppIconItem, times(3)).setImageDrawable(any(Drawable::class.java))
verify(coverItem, times(3)).setImageDrawable(any(Drawable::class.java))
}
@Test @Test
fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() { fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true) fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)

View File

@@ -22,6 +22,7 @@ import android.view.View
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.MeasurementInput
import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.animation.TransitionViewState import com.android.systemui.util.animation.TransitionViewState
@@ -55,6 +56,7 @@ class MediaViewControllerTest : SysuiTestCase() {
@Mock private lateinit var mediaTitleWidgetState: WidgetState @Mock private lateinit var mediaTitleWidgetState: WidgetState
@Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaSubTitleWidgetState: WidgetState
@Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState
@Mock private lateinit var mediaFlags: MediaFlags
val delta = 0.1F val delta = 0.1F
@@ -64,7 +66,13 @@ class MediaViewControllerTest : SysuiTestCase() {
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
mediaViewController = mediaViewController =
MediaViewController(context, configurationController, mediaHostStatesManager, logger) MediaViewController(
context,
configurationController,
mediaHostStatesManager,
logger,
mediaFlags,
)
} }
@Test @Test