From 73c4539cf0370de2828a47fff573f462fee60503 Mon Sep 17 00:00:00 2001 From: Michael Mikhail Date: Fri, 9 Dec 2022 10:20:53 +0000 Subject: [PATCH] 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 --- .../drawable/ic_media_explicit_indicator.xml | 26 +++++ .../res/layout/media_session_view.xml | 8 ++ packages/SystemUI/res/values/dimens.xml | 1 + .../res/xml/media_session_collapsed.xml | 20 +++- .../res/xml/media_session_expanded.xml | 20 +++- .../src/com/android/systemui/flags/Flags.kt | 3 + .../media/controls/models/player/MediaData.kt | 5 +- .../controls/models/player/MediaViewHolder.kt | 3 + .../controls/pipeline/MediaDataManager.kt | 20 +++- .../media/controls/ui/MediaControlPanel.java | 17 ++- .../media/controls/ui/MediaViewController.kt | 1 + .../media/controls/util/MediaFlags.kt | 3 + .../pipeline/MediaDataCombineLatestTest.java | 2 +- .../controls/pipeline/MediaDataManagerTest.kt | 102 ++++++++++++++++++ .../controls/ui/MediaControlPanelTest.kt | 38 +++++++ 15 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml 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 0000000000000..08c5aaf56bf77 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 95aefab328df5..abc83379950ab 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" /> + + + 1dp 2dp 24dp + 13dp 15dp 16dp diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 1eb621e0368b8..d9c81af54a12a 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" /> + + + + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" + app:layout_constraintVertical_bias="0" /> + + + + app:layout_constraintVertical_bias="0" /> (R.id.icon) val titleText = itemView.requireViewById(R.id.header_title) val artistText = itemView.requireViewById(R.id.header_artist) + val explicitIndicator = itemView.requireViewById(R.id.media_explicit_indicator) // Output switcher val seamless = itemView.requireViewById(R.id.media_seamless) @@ -123,6 +125,7 @@ class MediaViewHolder constructor(itemView: View) { 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 2dd339d409a62..415ebeedfbdee 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.Process 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 @@ class MediaDataManager( 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 @@ class MediaDataManager( hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } @@ -750,6 +756,15 @@ class MediaDataManager( 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 @@ class MediaDataManager( 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 15c34430f455b..f58090b2d433a 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.Process; 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.annotations.VisibleForTesting; 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.ColorUtilKt; 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 java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; +import kotlin.Triple; import kotlin.Unit; /** @@ -398,10 +400,11 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { 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 322421318cb80..d2c8e15868dc0 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 @@ constructor( 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 8d4931a5d08ce..5bc35caed515d 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 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { * [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 4d2d0f05b76aa..c0639f34484cd 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 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { 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 52b694fac07c8..c24c8c7f7cf61 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 @@ class MediaDataManagerTest : SysuiTestCase() { 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()) } @@ -299,6 +300,60 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(backgroundExecutor.numPending()).isEqualTo(1) } + @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() @@ -602,6 +657,53 @@ class MediaDataManagerTest : SysuiTestCase() { verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) } + @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 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 b65f5cb51aaf8..cfb19fc32bec9 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.constraintlayout.widget.ConstraintSet 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 @@ public class MediaControlPanelTest : SysuiTestCase() { @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 @@ public class MediaControlPanelTest : SysuiTestCase() { 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 @@ public class MediaControlPanelTest : SysuiTestCase() { 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 @@ public class MediaControlPanelTest : SysuiTestCase() { 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 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { + useRealConstraintSets() player.attachPlayer(viewHolder) player.bindPlayer(mediaData, PACKAGE) @@ -1036,12 +1042,44 @@ public class MediaControlPanelTest : SysuiTestCase() { 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) verify(mockAnimator, times(2)).start() } + @Test + fun bindTextWithExplicitIndicator() { + useRealConstraintSets() + val mediaDataWitExp = mediaData.copy(isExplicit = true) + player.attachPlayer(viewHolder) + player.bindPlayer(mediaDataWitExp, PACKAGE) + + // Capture animation handler + val captor = argumentCaptor() + 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")