Merge "Update media recommendation card layout" into tm-qpr-dev

This commit is contained in:
Michael Mikhail
2023-01-30 14:44:54 +00:00
committed by Android (Google) Code Review
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 -->
<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_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_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 -->
<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>
<!-- 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>
<!-- 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 ****** -->
<!-- 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.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">
<item name="android:layout_width">@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>
</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">
<item name="android:backgroundTint">@color/media_player_album_bg</item>
</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
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
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.widget.ImageView
import android.widget.TextView
import com.android.internal.widget.CachingIconView
import com.android.systemui.R
import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.ui.IlluminationDrawable
@@ -29,18 +30,15 @@ import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "RecommendationViewHolder"
/** 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
// Recommendation screen
val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
val mediaCoverItems =
listOf<ImageView>(
itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover2),
itemView.requireViewById(R.id.media_cover3)
)
lateinit var cardIcon: ImageView
lateinit var mediaAppIcons: List<CachingIconView>
lateinit var cardTitle: TextView
val mediaCoverContainers =
listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover1_container),
@@ -48,21 +46,45 @@ class RecommendationViewHolder private constructor(itemView: View) {
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)
)
if (updatedView) {
mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
} else {
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)
)
if (updatedView) {
mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
} else {
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)
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 ->
mediaCoverContainers.forEach { background.registerLightSource(it) }
background.registerLightSource(gutsViewHolder.cancel)
@@ -83,36 +105,52 @@ class RecommendationViewHolder private constructor(itemView: View) {
* @param parent Parent of inflated view.
*/
@JvmStatic
fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
fun create(
inflater: LayoutInflater,
parent: ViewGroup,
updatedView: Boolean,
): RecommendationViewHolder {
val itemView =
inflater.inflate(
R.layout.media_smartspace_recommendations,
parent,
false /* attachToRoot */
)
if (updatedView) {
inflater.inflate(
R.layout.media_recommendations,
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
// 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.
itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
return RecommendationViewHolder(itemView)
return RecommendationViewHolder(itemView, updatedView)
}
// Res Ids for the control components on the recommendation view.
val controlsIds =
setOf(
R.id.recommendation_card_icon,
R.id.media_rec_title,
R.id.media_cover1,
R.id.media_cover2,
R.id.media_cover3,
R.id.media_cover,
R.id.media_cover1_container,
R.id.media_cover2_container,
R.id.media_cover3_container,
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
R.id.media_title,
R.id.media_subtitle1,
R.id.media_subtitle2,
R.id.media_subtitle3
R.id.media_subtitle3,
R.id.media_subtitle,
)
val mediaTitlesAndSubtitlesIds =
@@ -120,9 +158,11 @@ class RecommendationViewHolder private constructor(itemView: View) {
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
R.id.media_title,
R.id.media_subtitle1,
R.id.media_subtitle2,
R.id.media_subtitle3
R.id.media_subtitle3,
R.id.media_subtitle,
)
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.pipeline.MediaDataManager
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.SmallHash
import com.android.systemui.plugins.ActivityStarter
@@ -88,7 +89,8 @@ constructor(
falsingManager: FalsingManager,
dumpManager: DumpManager,
private val logger: MediaUiEventLogger,
private val debugLogger: MediaCarouselControllerLogger
private val debugLogger: MediaCarouselControllerLogger,
private val mediaFlags: MediaFlags,
) : Dumpable {
/** The current width of the carousel */
private var currentCarouselWidth: Int = 0
@@ -647,7 +649,11 @@ constructor(
val newRecs = mediaControlPanelFactory.get()
newRecs.attachRecommendation(
RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
RecommendationViewHolder.create(
LayoutInflater.from(context),
mediaContent,
mediaFlags.isRecommendationCardUpdateEnabled()
)
)
newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
val lp =

View File

@@ -61,6 +61,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.constraintlayout.widget.ConstraintSet;
import com.android.internal.annotations.VisibleForTesting;
@@ -752,43 +753,16 @@ public class MediaControlPanel {
int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
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(() -> {
// Album art
ColorScheme mutableColorScheme = null;
Drawable artwork;
boolean isArtworkBound;
Icon artworkIcon = data.getArtwork();
WallpaperColors wallpaperColors = null;
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));
}
}
}
WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
if (wallpaperColors != null) {
mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
Drawable albumArt = getScaledBackground(artworkIcon, 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 });
artwork = addGradientToIcon(artworkIcon, mutableColorScheme, width, height);
isArtworkBound = true;
} else {
// 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,
int targetWidth, int targetHeight) {
Drawable drawable = transitionDrawable.getDrawable(layer);
@@ -1224,8 +1288,10 @@ public class MediaControlPanel {
PackageManager packageManager = mContext.getPackageManager();
// Set up media source app's logo.
Drawable icon = packageManager.getApplicationIcon(applicationInfo);
ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
headerLogoImageView.setImageDrawable(icon);
if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
headerLogoImageView.setImageDrawable(icon);
}
fetchAndUpdateRecommendationColors(icon);
// Set up media rec card's tap action if applicable.
@@ -1245,7 +1311,15 @@ public class MediaControlPanel {
// Set up media item cover.
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.
ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
@@ -1275,7 +1349,6 @@ public class MediaControlPanel {
recommendation.getTitle(), artistName, appName));
}
// Set up title
CharSequence title = recommendation.getTitle();
hasTitle |= !TextUtils.isEmpty(title);
@@ -1353,6 +1426,10 @@ public class MediaControlPanel {
int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
}
mRecommendationViewHolder.getRecommendations()
.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
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.recommendation.RecommendationViewHolder
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.util.animation.MeasurementOutput
import com.android.systemui.util.animation.TransitionLayout
@@ -45,7 +46,8 @@ constructor(
private val context: Context,
private val configurationController: ConfigurationController,
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)
}
TYPE.RECOMMENDATION -> {
collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
expandedLayout.load(context, R.xml.media_recommendation_expanded)
if (mediaFlags.isRecommendationCardUpdateEnabled()) {
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()

View File

@@ -51,4 +51,8 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
* whether the underlying notification was dismissed
*/
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.MediaDataManager
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.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
@@ -87,6 +88,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Mock lateinit var smartspaceMediaData: SmartspaceMediaData
@Mock lateinit var mediaCarousel: MediaScrollView
@Mock lateinit var pageIndicator: PageIndicator
@Mock lateinit var mediaFlags: MediaFlags
@Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
@Captor
lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener>
@@ -114,7 +116,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
falsingManager,
dumpManager,
logger,
debugLogger
debugLogger,
mediaFlags,
)
verify(configurationController).addCallback(capture(configListener))
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 coverContainer2: 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 coverItem2: ImageView
private lateinit var coverItem3: ImageView
@@ -220,6 +223,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
this.set(Flags.UMO_TURBULENCE_NOISE, false)
this.set(Flags.MEDIA_FALSING_PENALTY, true)
this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true)
this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
}
@JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -2058,6 +2062,51 @@ public class MediaControlPanelTest : SysuiTestCase() {
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
fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)

View File

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