Add an explicit media indicator icon to SysUI media controls

Checks for the value given by the key for explicit in MediaContants. And
applies all implementation necessary to show the explicit indicator as
an icon next to the artist name.

Bug: 221291466
Test: Manual - Checked apps that add explicit indicator to media.
Test: Checked with the media tester app.
Test: atest MediaDataManagerTest
Test: atest MediaControlPanelTest
Change-Id: I0721f098760e717b9ae32a8791fe68d5bac6e65f
diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml
new file mode 100644
index 0000000..08c5aaf
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="13dp"
+    android:height="13dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 95aefab..abc8337 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -147,6 +147,14 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content" />
 
+    <!-- Explicit Indicator -->
+    <com.android.internal.widget.CachingIconView
+        android:id="@+id/media_explicit_indicator"
+        android:layout_width="@dimen/qs_media_explicit_indicator_icon_size"
+        android:layout_height="@dimen/qs_media_explicit_indicator_icon_size"
+        android:src="@drawable/ic_media_explicit_indicator"
+        />
+
     <!-- Artist name -->
     <TextView
         android:id="@+id/header_artist"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 6841bf8..80d6cf2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1046,6 +1046,7 @@
     <dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
     <dimen name="qs_media_enabled_seekbar_height">2dp</dimen>
     <dimen name="qs_media_app_icon_size">24dp</dimen>
+    <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen>
 
     <dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen>
     <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen>
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index 1eb621e..d9c81af 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -66,6 +66,21 @@
         app:layout_constraintTop_toBottomOf="@id/icon"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintHorizontal_bias="0" />
+
+    <Constraint
+        android:id="@+id/media_explicit_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_padding"
+        android:layout_marginTop="0dp"
+        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintEnd_toStartOf="@id/header_artist"
+        app:layout_constraintTop_toTopOf="@id/header_artist"
+        app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed" />
+
     <Constraint
         android:id="@+id/header_artist"
         android:layout_width="wrap_content"
@@ -75,9 +90,8 @@
         app:layout_constraintEnd_toStartOf="@id/action_button_guideline"
         app:layout_constrainedWidth="true"
         app:layout_constraintTop_toBottomOf="@id/header_title"
-        app:layout_constraintStart_toStartOf="@id/header_title"
-        app:layout_constraintVertical_bias="0"
-        app:layout_constraintHorizontal_bias="0" />
+        app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
+        app:layout_constraintVertical_bias="0" />
 
     <Constraint
         android:id="@+id/actionPlayPause"
diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml
index 7de0a5e..0cdc0f9 100644
--- a/packages/SystemUI/res/xml/media_session_expanded.xml
+++ b/packages/SystemUI/res/xml/media_session_expanded.xml
@@ -58,6 +58,21 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintBottom_toTopOf="@id/header_artist"
         app:layout_constraintHorizontal_bias="0" />
+
+    <Constraint
+        android:id="@+id/media_explicit_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/qs_media_info_spacing"
+        android:layout_marginBottom="@dimen/qs_media_padding"
+        android:layout_marginTop="0dp"
+        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintEnd_toStartOf="@id/header_artist"
+        app:layout_constraintTop_toTopOf="@id/header_artist"
+        app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintHorizontal_chainStyle="packed"/>
+
     <Constraint
         android:id="@+id/header_artist"
         android:layout_width="wrap_content"
@@ -67,10 +82,9 @@
         android:layout_marginTop="0dp"
         app:layout_constrainedWidth="true"
         app:layout_constraintEnd_toStartOf="@id/actionPlayPause"
-        app:layout_constraintStart_toStartOf="@id/header_title"
+        app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
         app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top"
-        app:layout_constraintVertical_bias="0"
-        app:layout_constraintHorizontal_bias="0" />
+        app:layout_constraintVertical_bias="0" />
 
     <Constraint
         android:id="@+id/actionPlayPause"
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index ae8caad..8f33d95 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -327,6 +327,9 @@
     val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
         unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
 
+    // TODO(b/263512203): Tracking Bug
+    val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true)
+
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index f006442..be18cbe 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -88,7 +88,10 @@
     val instanceId: InstanceId,
 
     /** The UID of the app, used for logging */
-    val appUid: Int
+    val appUid: Int,
+
+    /** Whether explicit indicator exists */
+    val isExplicit: Boolean = false,
 ) {
     companion object {
         /** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
index a8f39fa9a..1c8bfd1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
@@ -24,6 +24,7 @@
 import android.widget.SeekBar
 import android.widget.TextView
 import androidx.constraintlayout.widget.Barrier
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.R
 import com.android.systemui.media.controls.models.GutsViewHolder
 import com.android.systemui.surfaceeffects.ripple.MultiRippleView
@@ -44,6 +45,7 @@
     val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
     val titleText = itemView.requireViewById<TextView>(R.id.header_title)
     val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
+    val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator)
 
     // Output switcher
     val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
@@ -123,6 +125,7 @@
                 R.id.app_name,
                 R.id.header_title,
                 R.id.header_artist,
+                R.id.media_explicit_indicator,
                 R.id.media_seamless,
                 R.id.media_progress_bar,
                 R.id.actionPlayPause,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 2dd339d..415ebee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -45,6 +45,7 @@
 import android.os.UserHandle
 import android.provider.Settings
 import android.service.notification.StatusBarNotification
+import android.support.v4.media.MediaMetadataCompat
 import android.text.TextUtils
 import android.util.Log
 import androidx.media.utils.MediaConstants
@@ -660,6 +661,10 @@
         val currentEntry = mediaEntries.get(packageName)
         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
         val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+        val isExplicit =
+            desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT &&
+                mediaFlags.isExplicitIndicatorEnabled()
 
         val mediaAction = getResumeMediaAction(resumeAction)
         val lastActive = systemClock.elapsedRealtime()
@@ -689,7 +694,8 @@
                     hasCheckedForResume = true,
                     lastActive = lastActive,
                     instanceId = instanceId,
-                    appUid = appUid
+                    appUid = appUid,
+                    isExplicit = isExplicit,
                 )
             )
         }
@@ -750,6 +756,15 @@
             song = HybridGroupManager.resolveTitle(notif)
         }
 
+        // Explicit Indicator
+        var isExplicit = false
+        if (mediaFlags.isExplicitIndicatorEnabled()) {
+            val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata)
+            isExplicit =
+                mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) ==
+                    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+        }
+
         // Artist name
         var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
         if (artist == null) {
@@ -851,7 +866,8 @@
                     isClearable = sbn.isClearable(),
                     lastActive = lastActive,
                     instanceId = instanceId,
-                    appUid = appUid
+                    appUid = appUid,
+                    isExplicit = isExplicit,
                 )
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 15c3443..f58090b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -50,7 +50,6 @@
 import android.os.Trace;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.Pair;
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
@@ -68,6 +67,7 @@
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.InstanceId;
+import com.android.internal.widget.CachingIconView;
 import com.android.settingslib.widget.AdaptiveIcon;
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.R;
@@ -113,6 +113,8 @@
 import com.android.systemui.util.animation.TransitionLayout;
 import com.android.systemui.util.time.SystemClock;
 
+import dagger.Lazy;
+
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
@@ -120,7 +122,7 @@
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
+import kotlin.Triple;
 import kotlin.Unit;
 
 /**
@@ -398,10 +400,11 @@
 
         TextView titleText = mMediaViewHolder.getTitleText();
         TextView artistText = mMediaViewHolder.getArtistText();
+        CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator();
         AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
-                Interpolators.EMPHASIZED_DECELERATE, titleText, artistText);
+                Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator);
         AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
-                Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
+                Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator);
 
         MultiRippleView multiRippleView = vh.getMultiRippleView();
         mMultiRippleController = new MultiRippleController(multiRippleView);
@@ -664,11 +667,15 @@
     private boolean bindSongMetadata(MediaData data) {
         TextView titleText = mMediaViewHolder.getTitleText();
         TextView artistText = mMediaViewHolder.getArtistText();
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
         return mMetadataAnimationHandler.setNext(
-            Pair.create(data.getSong(), data.getArtist()),
+            new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
             () -> {
                 titleText.setText(data.getSong());
                 artistText.setText(data.getArtist());
+                setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit());
+                setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit());
 
                 // refreshState is required here to resize the text views (and prevent ellipsis)
                 mMediaViewController.refreshState();
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 3224213..d2c8e15 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -80,6 +80,7 @@
             setOf(
                 R.id.header_title,
                 R.id.header_artist,
+                R.id.media_explicit_indicator,
                 R.id.actionPlayPause,
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 8d4931a..5bc35ca 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -42,4 +42,7 @@
      * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
      */
     fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
+
+    /** Check whether we show explicit indicator on UMO */
+    fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index 4d2d0f0..c0639f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
                 USER_ID, true, APP, null, ARTIST, TITLE, null,
                 new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
                 MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
-                InstanceId.fakeInstanceId(-1), -1);
+                InstanceId.fakeInstanceId(-1), -1, false);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 52b694f..c24c8c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -228,6 +228,7 @@
         whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
         whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
+        whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
         whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
     }
 
@@ -300,6 +301,60 @@
     }
 
     @Test
+    fun testLoadMetadata_withExplicitIndicator() {
+        val metadata =
+            MediaMetadata.Builder().run {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+                putLong(
+                    MediaConstants.METADATA_KEY_IS_EXPLICIT,
+                    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+                )
+                build()
+            }
+        whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
+        whenever(controller.metadata).thenReturn(metadata)
+
+        mediaDataManager.addListener(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.isExplicit).isTrue()
+    }
+
+    @Test
+    fun testOnMetaDataLoaded_withoutExplicitIndicator() {
+        whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+
+        mediaDataManager.addListener(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.isExplicit).isFalse()
+    }
+
+    @Test
     fun testOnMetaDataLoaded_callsListener() {
         addNotificationAndLoad()
         verify(logger)
@@ -603,6 +658,53 @@
     }
 
     @Test
+    fun testAddResumptionControls_withExplicitIndicator() {
+        val bundle = Bundle()
+        // WHEN resumption controls are added with explicit indicator
+        bundle.putLong(
+            MediaConstants.METADATA_KEY_IS_EXPLICIT,
+            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
+        )
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                setExtras(bundle)
+                build()
+            }
+        val currentTime = clock.elapsedRealtime()
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        // THEN the media data indicates that it is for resumption
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.song).isEqualTo(SESSION_TITLE)
+        assertThat(data.app).isEqualTo(APP_NAME)
+        assertThat(data.actions).hasSize(1)
+        assertThat(data.semanticActions!!.playOrPause).isNotNull()
+        assertThat(data.lastActive).isAtLeast(currentTime)
+        assertThat(data.isExplicit).isTrue()
+        verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
     fun testResumptionDisabled_dismissesResumeControls() {
         // WHEN there are resume controls and resumption is switched off
         val desc =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index b65f5cb..cfb19fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -54,6 +54,7 @@
 import androidx.lifecycle.LiveData
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
+import com.android.internal.widget.CachingIconView
 import com.android.systemui.ActivityIntentHelper
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -154,6 +155,7 @@
     @Mock private lateinit var albumView: ImageView
     private lateinit var titleText: TextView
     private lateinit var artistText: TextView
+    private lateinit var explicitIndicator: CachingIconView
     private lateinit var seamless: ViewGroup
     private lateinit var seamlessButton: View
     @Mock private lateinit var seamlessBackground: RippleDrawable
@@ -216,6 +218,7 @@
             this.set(Flags.UMO_SURFACE_RIPPLE, false)
             this.set(Flags.UMO_TURBULENCE_NOISE, false)
             this.set(Flags.MEDIA_FALSING_PENALTY, true)
+            this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true)
         }
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -350,6 +353,7 @@
         appIcon = ImageView(context)
         titleText = TextView(context)
         artistText = TextView(context)
+        explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator }
         seamless = FrameLayout(context)
         seamless.foreground = seamlessBackground
         seamlessButton = View(context)
@@ -396,6 +400,7 @@
         whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
         whenever(viewHolder.titleText).thenReturn(titleText)
         whenever(viewHolder.artistText).thenReturn(artistText)
+        whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator)
         whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
         whenever(viewHolder.seamless).thenReturn(seamless)
         whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
@@ -1019,6 +1024,7 @@
 
     @Test
     fun bindText() {
+        useRealConstraintSets()
         player.attachPlayer(viewHolder)
         player.bindPlayer(mediaData, PACKAGE)
 
@@ -1036,6 +1042,8 @@
         handler.onAnimationEnd(mockAnimator)
         assertThat(titleText.getText()).isEqualTo(TITLE)
         assertThat(artistText.getText()).isEqualTo(ARTIST)
+        assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE)
 
         // Rebinding should not trigger animation
         player.bindPlayer(mediaData, PACKAGE)
@@ -1043,6 +1051,36 @@
     }
 
     @Test
+    fun bindTextWithExplicitIndicator() {
+        useRealConstraintSets()
+        val mediaDataWitExp = mediaData.copy(isExplicit = true)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaDataWitExp, PACKAGE)
+
+        // Capture animation handler
+        val captor = argumentCaptor<Animator.AnimatorListener>()
+        verify(mockAnimator, times(2)).addListener(captor.capture())
+        val handler = captor.value
+
+        // Validate text views unchanged but animation started
+        assertThat(titleText.getText()).isEqualTo("")
+        assertThat(artistText.getText()).isEqualTo("")
+        verify(mockAnimator, times(1)).start()
+
+        // Binding only after animator runs
+        handler.onAnimationEnd(mockAnimator)
+        assertThat(titleText.getText()).isEqualTo(TITLE)
+        assertThat(artistText.getText()).isEqualTo(ARTIST)
+        assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(collapsedSet.getVisibility(explicitIndicator.id))
+            .isEqualTo(ConstraintSet.VISIBLE)
+
+        // Rebinding should not trigger animation
+        player.bindPlayer(mediaData, PACKAGE)
+        verify(mockAnimator, times(3)).start()
+    }
+
+    @Test
     fun bindTextInterrupted() {
         val data0 = mediaData.copy(artist = "ARTIST_0")
         val data1 = mediaData.copy(artist = "ARTIST_1")