Merge "Add an explicit media indicator icon to SysUI media controls" into tm-qpr-dev am: 4fd565a8ae

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

Change-Id: Iebb7a33695836dfed215edf81a8d59d67170bb43
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Michael Mikhail
2023-01-12 18:51:45 +00:00
committed by Automerger Merge Worker
15 changed files with 254 additions and 15 deletions

View File

@@ -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>

View File

@@ -147,6 +147,14 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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 --> <!-- Artist name -->
<TextView <TextView
android:id="@+id/header_artist" android:id="@+id/header_artist"

View File

@@ -1055,6 +1055,7 @@
<dimen name="qs_media_disabled_seekbar_height">1dp</dimen> <dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
<dimen name="qs_media_enabled_seekbar_height">2dp</dimen> <dimen name="qs_media_enabled_seekbar_height">2dp</dimen>
<dimen name="qs_media_app_icon_size">24dp</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_enabled_seekbar_vertical_padding">15dp</dimen>
<dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen> <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen>

View File

@@ -66,6 +66,21 @@
app:layout_constraintTop_toBottomOf="@id/icon" app:layout_constraintTop_toBottomOf="@id/icon"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintHorizontal_bias="0" /> 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 <Constraint
android:id="@+id/header_artist" android:id="@+id/header_artist"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -75,9 +90,8 @@
app:layout_constraintEnd_toStartOf="@id/action_button_guideline" app:layout_constraintEnd_toStartOf="@id/action_button_guideline"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintTop_toBottomOf="@id/header_title" app:layout_constraintTop_toBottomOf="@id/header_title"
app:layout_constraintStart_toStartOf="@id/header_title" app:layout_constraintStart_toEndOf="@id/media_explicit_indicator"
app:layout_constraintVertical_bias="0" app:layout_constraintVertical_bias="0" />
app:layout_constraintHorizontal_bias="0" />
<Constraint <Constraint
android:id="@+id/actionPlayPause" android:id="@+id/actionPlayPause"

View File

@@ -58,6 +58,21 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintBottom_toTopOf="@id/header_artist"
app:layout_constraintHorizontal_bias="0" /> 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 <Constraint
android:id="@+id/header_artist" android:id="@+id/header_artist"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -67,10 +82,9 @@
android:layout_marginTop="0dp" android:layout_marginTop="0dp"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/actionPlayPause" 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_constraintBottom_toTopOf="@id/media_action_barrier_top"
app:layout_constraintVertical_bias="0" app:layout_constraintVertical_bias="0" />
app:layout_constraintHorizontal_bias="0" />
<Constraint <Constraint
android:id="@+id/actionPlayPause" android:id="@+id/actionPlayPause"

View File

@@ -336,6 +336,9 @@ object Flags {
val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true) 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 // 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

@@ -88,7 +88,10 @@ data class MediaData(
val instanceId: InstanceId, val instanceId: InstanceId,
/** The UID of the app, used for logging */ /** The UID of the app, used for logging */
val appUid: Int val appUid: Int,
/** Whether explicit indicator exists */
val isExplicit: Boolean = false,
) { ) {
companion object { companion object {
/** Media is playing on the local device */ /** Media is playing on the local device */

View File

@@ -24,6 +24,7 @@ import android.widget.ImageView
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.Barrier
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.surfaceeffects.ripple.MultiRippleView import com.android.systemui.surfaceeffects.ripple.MultiRippleView
@@ -44,6 +45,7 @@ class MediaViewHolder constructor(itemView: View) {
val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
val titleText = itemView.requireViewById<TextView>(R.id.header_title) val titleText = itemView.requireViewById<TextView>(R.id.header_title)
val artistText = itemView.requireViewById<TextView>(R.id.header_artist) val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator)
// Output switcher // Output switcher
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
@@ -123,6 +125,7 @@ class MediaViewHolder constructor(itemView: View) {
R.id.app_name, R.id.app_name,
R.id.header_title, R.id.header_title,
R.id.header_artist, R.id.header_artist,
R.id.media_explicit_indicator,
R.id.media_seamless, R.id.media_seamless,
R.id.media_progress_bar, R.id.media_progress_bar,
R.id.actionPlayPause, R.id.actionPlayPause,

View File

@@ -45,6 +45,7 @@ import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import android.provider.Settings import android.provider.Settings
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import android.support.v4.media.MediaMetadataCompat
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
@@ -660,6 +661,10 @@ class MediaDataManager(
val currentEntry = mediaEntries.get(packageName) val currentEntry = mediaEntries.get(packageName)
val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
val appUid = currentEntry?.appUid ?: Process.INVALID_UID 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 mediaAction = getResumeMediaAction(resumeAction)
val lastActive = systemClock.elapsedRealtime() val lastActive = systemClock.elapsedRealtime()
@@ -689,7 +694,8 @@ class MediaDataManager(
hasCheckedForResume = true, hasCheckedForResume = true,
lastActive = lastActive, lastActive = lastActive,
instanceId = instanceId, instanceId = instanceId,
appUid = appUid appUid = appUid,
isExplicit = isExplicit,
) )
) )
} }
@@ -750,6 +756,15 @@ class MediaDataManager(
song = HybridGroupManager.resolveTitle(notif) 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 // Artist name
var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
if (artist == null) { if (artist == null) {
@@ -851,7 +866,8 @@ class MediaDataManager(
isClearable = sbn.isClearable(), isClearable = sbn.isClearable(),
lastActive = lastActive, lastActive = lastActive,
instanceId = instanceId, instanceId = instanceId,
appUid = appUid appUid = appUid,
isExplicit = isExplicit,
) )
) )
} }

View File

@@ -50,7 +50,6 @@ import android.os.Process;
import android.os.Trace; import android.os.Trace;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -68,6 +67,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils; import com.android.internal.graphics.ColorUtils;
import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceId;
import com.android.internal.widget.CachingIconView;
import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.ActivityIntentHelper; import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.R; 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.animation.TransitionLayout;
import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClock;
import dagger.Lazy;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -120,7 +122,7 @@ import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import dagger.Lazy; import kotlin.Triple;
import kotlin.Unit; import kotlin.Unit;
/** /**
@@ -398,10 +400,11 @@ public class MediaControlPanel {
TextView titleText = mMediaViewHolder.getTitleText(); TextView titleText = mMediaViewHolder.getTitleText();
TextView artistText = mMediaViewHolder.getArtistText(); TextView artistText = mMediaViewHolder.getArtistText();
CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator();
AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter, 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, AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator);
MultiRippleView multiRippleView = vh.getMultiRippleView(); MultiRippleView multiRippleView = vh.getMultiRippleView();
mMultiRippleController = new MultiRippleController(multiRippleView); mMultiRippleController = new MultiRippleController(multiRippleView);
@@ -664,11 +667,15 @@ public class MediaControlPanel {
private boolean bindSongMetadata(MediaData data) { private boolean bindSongMetadata(MediaData data) {
TextView titleText = mMediaViewHolder.getTitleText(); TextView titleText = mMediaViewHolder.getTitleText();
TextView artistText = mMediaViewHolder.getArtistText(); TextView artistText = mMediaViewHolder.getArtistText();
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
return mMetadataAnimationHandler.setNext( return mMetadataAnimationHandler.setNext(
Pair.create(data.getSong(), data.getArtist()), new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
() -> { () -> {
titleText.setText(data.getSong()); titleText.setText(data.getSong());
artistText.setText(data.getArtist()); 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) // refreshState is required here to resize the text views (and prevent ellipsis)
mMediaViewController.refreshState(); mMediaViewController.refreshState();

View File

@@ -77,6 +77,7 @@ constructor(
setOf( setOf(
R.id.header_title, R.id.header_title,
R.id.header_artist, R.id.header_artist,
R.id.media_explicit_indicator,
R.id.actionPlayPause, R.id.actionPlayPause,
) )

View File

@@ -42,4 +42,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
* [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information. * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
*/ */
fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES) fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
/** Check whether we show explicit indicator on UMO */
fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR)
} }

View File

@@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
USER_ID, true, APP, null, ARTIST, TITLE, null, USER_ID, true, APP, null, ARTIST, TITLE, null,
new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, 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); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
} }

View File

@@ -228,6 +228,7 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
} }
@@ -299,6 +300,60 @@ class MediaDataManagerTest : SysuiTestCase() {
assertThat(backgroundExecutor.numPending()).isEqualTo(1) 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 @Test
fun testOnMetaDataLoaded_callsListener() { fun testOnMetaDataLoaded_callsListener() {
addNotificationAndLoad() addNotificationAndLoad()
@@ -602,6 +657,53 @@ class MediaDataManagerTest : SysuiTestCase() {
verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) 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 @Test
fun testResumptionDisabled_dismissesResumeControls() { fun testResumptionDisabled_dismissesResumeControls() {
// WHEN there are resume controls and resumption is switched off // WHEN there are resume controls and resumption is switched off

View File

@@ -54,6 +54,7 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceId
import com.android.internal.widget.CachingIconView
import com.android.systemui.ActivityIntentHelper import com.android.systemui.ActivityIntentHelper
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
@@ -154,6 +155,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var albumView: ImageView @Mock private lateinit var albumView: ImageView
private lateinit var titleText: TextView private lateinit var titleText: TextView
private lateinit var artistText: TextView private lateinit var artistText: TextView
private lateinit var explicitIndicator: CachingIconView
private lateinit var seamless: ViewGroup private lateinit var seamless: ViewGroup
private lateinit var seamlessButton: View private lateinit var seamlessButton: View
@Mock private lateinit var seamlessBackground: RippleDrawable @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_SURFACE_RIPPLE, false)
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)
} }
@JvmField @Rule val mockito = MockitoJUnit.rule() @JvmField @Rule val mockito = MockitoJUnit.rule()
@@ -350,6 +353,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
appIcon = ImageView(context) appIcon = ImageView(context)
titleText = TextView(context) titleText = TextView(context)
artistText = TextView(context) artistText = TextView(context)
explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator }
seamless = FrameLayout(context) seamless = FrameLayout(context)
seamless.foreground = seamlessBackground seamless.foreground = seamlessBackground
seamlessButton = View(context) seamlessButton = View(context)
@@ -396,6 +400,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(albumView.foreground).thenReturn(mock(Drawable::class.java)) whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
whenever(viewHolder.titleText).thenReturn(titleText) whenever(viewHolder.titleText).thenReturn(titleText)
whenever(viewHolder.artistText).thenReturn(artistText) whenever(viewHolder.artistText).thenReturn(artistText)
whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator)
whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
whenever(viewHolder.seamless).thenReturn(seamless) whenever(viewHolder.seamless).thenReturn(seamless)
whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
@@ -1019,6 +1024,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun bindText() { fun bindText() {
useRealConstraintSets()
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(mediaData, PACKAGE) player.bindPlayer(mediaData, PACKAGE)
@@ -1036,12 +1042,44 @@ public class MediaControlPanelTest : SysuiTestCase() {
handler.onAnimationEnd(mockAnimator) handler.onAnimationEnd(mockAnimator)
assertThat(titleText.getText()).isEqualTo(TITLE) assertThat(titleText.getText()).isEqualTo(TITLE)
assertThat(artistText.getText()).isEqualTo(ARTIST) 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 // Rebinding should not trigger animation
player.bindPlayer(mediaData, PACKAGE) player.bindPlayer(mediaData, PACKAGE)
verify(mockAnimator, times(2)).start() 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<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 @Test
fun bindTextInterrupted() { fun bindTextInterrupted() {
val data0 = mediaData.copy(artist = "ARTIST_0") val data0 = mediaData.copy(artist = "ARTIST_0")