Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 680f6575 authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Automerger Merge Worker
Browse files

Merge "Prevent media controls with no title" into udc-dev am: 910e30bf am: 9c5590df

parents 2794fd6c 9c5590df
Loading
Loading
Loading
Loading
+24 −0
Original line number Original line Diff line number Diff line
@@ -571,6 +571,15 @@ public class StatusBarManager {
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L;
    private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L;


    /**
     * Media controls based on {@link android.app.Notification.MediaStyle} notifications will be
     * required to include a non-empty title, either in the {@link android.media.MediaMetadata} or
     * notification title.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    private static final long MEDIA_CONTROL_REQUIRES_TITLE = 274775190L;

    @UnsupportedAppUsage
    @UnsupportedAppUsage
    private Context mContext;
    private Context mContext;
    private IStatusBarService mService;
    private IStatusBarService mService;
@@ -1216,6 +1225,21 @@ public class StatusBarManager {
        return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, user);
        return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, user);
    }
    }


    /**
     * Checks whether the given package must include a non-empty title for its media controls.
     *
     * @param packageName App posting media controls
     * @param user Current user handle
     * @return true if the app is required to provide a non-empty title
     *
     * @hide
     */
    @RequiresPermission(allOf = {android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
            android.Manifest.permission.LOG_COMPAT_CHANGE})
    public static boolean isMediaTitleRequiredForApp(String packageName, UserHandle user) {
        return CompatChanges.isChangeEnabled(MEDIA_CONTROL_REQUIRES_TITLE, packageName, user);
    }

    /**
    /**
     * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)}
     * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)}
     * a system activity that captures content on the screen to take a screenshot.
     * a system activity that captures content on the screen to take a screenshot.
+3 −1
Original line number Original line Diff line number Diff line
@@ -2583,7 +2583,7 @@
    <!-- Title for media controls [CHAR_LIMIT=50] -->
    <!-- Title for media controls [CHAR_LIMIT=50] -->
    <string name="controls_media_title">Media</string>
    <string name="controls_media_title">Media</string>
    <!-- Explanation for closing controls associated with a specific media session [CHAR_LIMIT=50] -->
    <!-- Explanation for closing controls associated with a specific media session [CHAR_LIMIT=50] -->
    <string name="controls_media_close_session">Hide this media control for <xliff:g id="app_name" example="YouTube Music">%1$s</xliff:g>?</string>
    <string name="controls_media_close_session">Hide this media control for <xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g>?</string>
    <!-- Explanation that controls associated with a specific media session are active [CHAR_LIMIT=50] -->
    <!-- Explanation that controls associated with a specific media session are active [CHAR_LIMIT=50] -->
    <string name="controls_media_active_session">The current media session cannot be hidden.</string>
    <string name="controls_media_active_session">The current media session cannot be hidden.</string>
    <!-- Label for a button that will hide media controls [CHAR_LIMIT=30] -->
    <!-- Label for a button that will hide media controls [CHAR_LIMIT=30] -->
@@ -2596,6 +2596,8 @@
    <string name="controls_media_playing_item_description"><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> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
    <string name="controls_media_playing_item_description"><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> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
    <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
    <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
    <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] -->
    <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string>


    <!-- Description for button in media controls. Pressing button starts playback [CHAR_LIMIT=NONE] -->
    <!-- Description for button in media controls. Pressing button starts playback [CHAR_LIMIT=NONE] -->
    <string name="controls_media_button_play">Play</string>
    <string name="controls_media_button_play">Play</string>
+55 −22
Original line number Original line Diff line number Diff line
@@ -43,6 +43,7 @@ import android.media.session.PlaybackState
import android.net.Uri
import android.net.Uri
import android.os.Parcelable
import android.os.Parcelable
import android.os.Process
import android.os.Process
import android.os.RemoteException
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
@@ -52,6 +53,7 @@ import android.util.Log
import android.util.Pair as APair
import android.util.Pair as APair
import androidx.media.utils.MediaConstants
import androidx.media.utils.MediaConstants
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceId
import com.android.internal.statusbar.IStatusBarService
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Dumpable
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.R
@@ -137,6 +139,8 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA =
        expiryTimeMs = 0,
        expiryTimeMs = 0,
    )
    )


const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank."

fun isMediaNotification(sbn: StatusBarNotification): Boolean {
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
    return sbn.notification.isMediaNotification()
    return sbn.notification.isMediaNotification()
}
}
@@ -181,6 +185,7 @@ class MediaDataManager(
    private val logger: MediaUiEventLogger,
    private val logger: MediaUiEventLogger,
    private val smartspaceManager: SmartspaceManager,
    private val smartspaceManager: SmartspaceManager,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
    private val statusBarService: IStatusBarService,
) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {


    companion object {
    companion object {
@@ -252,6 +257,7 @@ class MediaDataManager(
        mediaFlags: MediaFlags,
        mediaFlags: MediaFlags,
        logger: MediaUiEventLogger,
        logger: MediaUiEventLogger,
        smartspaceManager: SmartspaceManager,
        smartspaceManager: SmartspaceManager,
        statusBarService: IStatusBarService,
        keyguardUpdateMonitor: KeyguardUpdateMonitor,
        keyguardUpdateMonitor: KeyguardUpdateMonitor,
    ) : this(
    ) : this(
        context,
        context,
@@ -277,6 +283,7 @@ class MediaDataManager(
        logger,
        logger,
        smartspaceManager,
        smartspaceManager,
        keyguardUpdateMonitor,
        keyguardUpdateMonitor,
        statusBarService,
    )
    )


    private val appChangeReceiver =
    private val appChangeReceiver =
@@ -378,21 +385,21 @@ class MediaDataManager(


    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
        if (useQsMediaPlayer && isMediaNotification(sbn)) {
        if (useQsMediaPlayer && isMediaNotification(sbn)) {
            var logEvent = false
            var isNewlyActiveEntry = false
            Assert.isMainThread()
            Assert.isMainThread()
            val oldKey = findExistingEntry(key, sbn.packageName)
            val oldKey = findExistingEntry(key, sbn.packageName)
            if (oldKey == null) {
            if (oldKey == null) {
                val instanceId = logger.getNewInstanceId()
                val instanceId = logger.getNewInstanceId()
                val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId)
                val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId)
                mediaEntries.put(key, temp)
                mediaEntries.put(key, temp)
                logEvent = true
                isNewlyActiveEntry = true
            } else if (oldKey != key) {
            } else if (oldKey != key) {
                // Resume -> active conversion; move to new key
                // Resume -> active conversion; move to new key
                val oldData = mediaEntries.remove(oldKey)!!
                val oldData = mediaEntries.remove(oldKey)!!
                logEvent = true
                isNewlyActiveEntry = true
                mediaEntries.put(key, oldData)
                mediaEntries.put(key, oldData)
            }
            }
            loadMediaData(key, sbn, oldKey, logEvent)
            loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
        } else {
        } else {
            onNotificationRemoved(key)
            onNotificationRemoved(key)
        }
        }
@@ -475,9 +482,9 @@ class MediaDataManager(
        key: String,
        key: String,
        sbn: StatusBarNotification,
        sbn: StatusBarNotification,
        oldKey: String?,
        oldKey: String?,
        logEvent: Boolean = false
        isNewlyActiveEntry: Boolean = false,
    ) {
    ) {
        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) }
        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
    }
    }


    /** Add a listener for changes in this class */
    /** Add a listener for changes in this class */
@@ -601,10 +608,12 @@ class MediaDataManager(
        }
        }
    }
    }


    private fun removeEntry(key: String) {
    private fun removeEntry(key: String, logEvent: Boolean = true) {
        mediaEntries.remove(key)?.let {
        mediaEntries.remove(key)?.let {
            if (logEvent) {
                logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
                logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
            }
            }
        }
        notifyMediaDataRemoved(key)
        notifyMediaDataRemoved(key)
    }
    }


@@ -751,7 +760,7 @@ class MediaDataManager(
        key: String,
        key: String,
        sbn: StatusBarNotification,
        sbn: StatusBarNotification,
        oldKey: String?,
        oldKey: String?,
        logEvent: Boolean = false
        isNewlyActiveEntry: Boolean = false,
    ) {
    ) {
        val token =
        val token =
            sbn.notification.extras.getParcelable(
            sbn.notification.extras.getParcelable(
@@ -772,6 +781,42 @@ class MediaDataManager(
            )
            )
                ?: getAppInfoFromPackage(sbn.packageName)
                ?: getAppInfoFromPackage(sbn.packageName)


        // App name
        val appName = getAppName(sbn, appInfo)

        // Song name
        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
        if (song == null) {
            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
        }
        if (song == null) {
            song = HybridGroupManager.resolveTitle(notif)
        }
        if (song.isNullOrBlank()) {
            if (mediaFlags.isMediaTitleRequired(sbn.packageName, sbn.user)) {
                // App is required to provide a title: cancel the underlying notification
                try {
                    statusBarService.onNotificationError(
                        sbn.packageName,
                        sbn.tag,
                        sbn.id,
                        sbn.uid,
                        sbn.initialPid,
                        MEDIA_TITLE_ERROR_MESSAGE,
                        sbn.user.identifier
                    )
                } catch (e: RemoteException) {
                    Log.e(TAG, "cancelNotification failed: $e")
                }
                // Only add log for media removed if active media is updated with invalid title.
                foregroundExecutor.execute { removeEntry(key, !isNewlyActiveEntry) }
                return
            } else {
                // For apps that don't have the title requirement yet, add a placeholder
                song = context.getString(R.string.controls_media_empty_title, appName)
            }
        }

        // Album art
        // Album art
        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
        if (artworkBitmap == null) {
        if (artworkBitmap == null) {
@@ -787,21 +832,9 @@ class MediaDataManager(
                Icon.createWithBitmap(artworkBitmap)
                Icon.createWithBitmap(artworkBitmap)
            }
            }


        // App name
        val appName = getAppName(sbn, appInfo)

        // App Icon
        // App Icon
        val smallIcon = sbn.notification.smallIcon
        val smallIcon = sbn.notification.smallIcon


        // Song name
        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
        if (song == null) {
            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
        }
        if (song == null) {
            song = HybridGroupManager.resolveTitle(notif)
        }

        // Explicit Indicator
        // Explicit Indicator
        var isExplicit = false
        var isExplicit = false
        if (mediaFlags.isExplicitIndicatorEnabled()) {
        if (mediaFlags.isExplicitIndicatorEnabled()) {
@@ -873,7 +906,7 @@ class MediaDataManager(
        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
        val appUid = appInfo?.uid ?: Process.INVALID_UID
        val appUid = appInfo?.uid ?: Process.INVALID_UID


        if (logEvent) {
        if (isNewlyActiveEntry) {
            logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
            logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
        } else if (playbackLocation != currentEntry?.playbackLocation) {
        } else if (playbackLocation != currentEntry?.playbackLocation) {
+5 −0
Original line number Original line Diff line number Diff line
@@ -64,4 +64,9 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {


    /** Check whether we allow remote media to generate resume controls */
    /** Check whether we allow remote media to generate resume controls */
    fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME)
    fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME)

    /** Check whether app is required to provide a non-empty media title */
    fun isMediaTitleRequired(packageName: String, user: UserHandle): Boolean {
        return StatusBarManager.isMediaTitleRequiredForApp(packageName, user)
    }
}
}
+206 −11
Original line number Original line Diff line number Diff line
@@ -25,6 +25,7 @@ import android.app.smartspace.SmartspaceConfig
import android.app.smartspace.SmartspaceManager
import android.app.smartspace.SmartspaceManager
import android.app.smartspace.SmartspaceTarget
import android.app.smartspace.SmartspaceTarget
import android.content.Intent
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.graphics.drawable.Icon
import android.media.MediaDescription
import android.media.MediaDescription
@@ -40,6 +41,7 @@ import android.testing.TestableLooper.RunWithLooper
import androidx.media.utils.MediaConstants
import androidx.media.utils.MediaConstants
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.statusbar.IStatusBarService
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.R
import com.android.systemui.R
@@ -76,6 +78,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.verify
@@ -130,6 +133,7 @@ class MediaDataManagerTest : SysuiTestCase() {
    @Mock lateinit var activityStarter: ActivityStarter
    @Mock lateinit var activityStarter: ActivityStarter
    @Mock lateinit var smartspaceManager: SmartspaceManager
    @Mock lateinit var smartspaceManager: SmartspaceManager
    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock lateinit var statusBarService: IStatusBarService
    lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
    lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
    @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
    @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
    @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
    @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
@@ -192,7 +196,8 @@ class MediaDataManagerTest : SysuiTestCase() {
                mediaFlags = mediaFlags,
                mediaFlags = mediaFlags,
                logger = logger,
                logger = logger,
                smartspaceManager = smartspaceManager,
                smartspaceManager = smartspaceManager,
                keyguardUpdateMonitor = keyguardUpdateMonitor
                keyguardUpdateMonitor = keyguardUpdateMonitor,
                statusBarService = statusBarService,
            )
            )
        verify(tunerService)
        verify(tunerService)
            .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
            .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
@@ -517,19 +522,211 @@ class MediaDataManagerTest : SysuiTestCase() {
    }
    }


    @Test
    @Test
    fun testOnNotificationRemoved_emptyTitle_notConverted() {
    fun testOnNotificationAdded_emptyTitle_isRequired_notLoaded() {
        // GIVEN that the manager has a notification with a resume action and empty title.
        // When the manager has a notification with an empty title, and the app is required
        // to include a non-empty title
        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
        whenever(controller.metadata)
        whenever(controller.metadata)
            .thenReturn(
            .thenReturn(
                metadataBuilder
                metadataBuilder
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
                    .build()
                    .build()
            )
            )
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)

        // Then the media control is not added and we report a notification error
        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
        verify(statusBarService)
            .onNotificationError(
                eq(PACKAGE_NAME),
                eq(mediaNotification.tag),
                eq(mediaNotification.id),
                eq(mediaNotification.uid),
                eq(mediaNotification.initialPid),
                eq(MEDIA_TITLE_ERROR_MESSAGE),
                eq(mediaNotification.user.identifier)
            )
        verify(listener, never())
            .onMediaDataLoaded(
                eq(KEY),
                eq(null),
                capture(mediaDataCaptor),
                eq(true),
                eq(0),
                eq(false)
            )
        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
        verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any())
    }

    @Test
    fun testOnNotificationAdded_blankTitle_isRequired_notLoaded() {
        // When the manager has a notification with a blank title, and the app is required
        // to include a non-empty title
        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
        whenever(controller.metadata)
            .thenReturn(
                metadataBuilder
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
                    .build()
            )
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)

        // Then the media control is not added and we report a notification error
        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
        verify(statusBarService)
            .onNotificationError(
                eq(PACKAGE_NAME),
                eq(mediaNotification.tag),
                eq(mediaNotification.id),
                eq(mediaNotification.uid),
                eq(mediaNotification.initialPid),
                eq(MEDIA_TITLE_ERROR_MESSAGE),
                eq(mediaNotification.user.identifier)
            )
        verify(listener, never())
            .onMediaDataLoaded(
                eq(KEY),
                eq(null),
                capture(mediaDataCaptor),
                eq(true),
                eq(0),
                eq(false)
            )
        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
        verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any())
    }

    @Test
    fun testOnNotificationUpdated_invalidTitle_isRequired_logMediaRemoved() {
        // When the app is required to provide a non-blank title, and updates a previously valid
        // title to an empty one
        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
        addNotificationAndLoad()
        val data = mediaDataCaptor.value

        verify(listener)
            .onMediaDataLoaded(
                eq(KEY),
                eq(null),
                capture(mediaDataCaptor),
                eq(true),
                eq(0),
                eq(false)
            )

        reset(listener)
        whenever(controller.metadata)
            .thenReturn(
                metadataBuilder
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
                    .build()
            )
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)

        // Then the media control is removed
        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
        verify(statusBarService)
            .onNotificationError(
                eq(PACKAGE_NAME),
                eq(mediaNotification.tag),
                eq(mediaNotification.id),
                eq(mediaNotification.uid),
                eq(mediaNotification.initialPid),
                eq(MEDIA_TITLE_ERROR_MESSAGE),
                eq(mediaNotification.user.identifier)
            )
        verify(listener, never())
            .onMediaDataLoaded(
                eq(KEY),
                eq(null),
                capture(mediaDataCaptor),
                eq(true),
                eq(0),
                eq(false)
            )
        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
    }

    @Test
    fun testOnNotificationAdded_emptyTitle_notRequired_hasPlaceholder() {
        // When the manager has a notification with an empty title, and the app is not
        // required to include a non-empty title
        val mockPackageManager = mock(PackageManager::class.java)
        context.setMockPackageManager(mockPackageManager)
        whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false)
        whenever(controller.metadata)
            .thenReturn(
                metadataBuilder
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
                    .build()
            )
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)

        // Then a media control is created with a placeholder title string
        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)
            )
        val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME)
        assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle)
    }

    @Test
    fun testOnNotificationAdded_blankTitle_notRequired_hasPlaceholder() {
        // GIVEN that the manager has a notification with a blank title, and the app is not
        // required to include a non-empty title
        val mockPackageManager = mock(PackageManager::class.java)
        context.setMockPackageManager(mockPackageManager)
        whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
        whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false)
        whenever(controller.metadata)
            .thenReturn(
                metadataBuilder
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
                    .build()
            )
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)

        // Then a media control is created with a placeholder title string
        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)
            )
        val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME)
        assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle)
    }

    @Test
    fun testOnNotificationRemoved_emptyTitle_notConverted() {
        // GIVEN that the manager has a notification with a resume action and empty title.
        addNotificationAndLoad()
        addNotificationAndLoad()
        val data = mediaDataCaptor.value
        val data = mediaDataCaptor.value
        val instanceId = data.instanceId
        val instanceId = data.instanceId
        assertThat(data.resumption).isFalse()
        assertThat(data.resumption).isFalse()
        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
        mediaDataManager.onMediaDataLoaded(
            KEY,
            null,
            data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {})
        )


        // WHEN the notification is removed
        // WHEN the notification is removed
        reset(listener)
        reset(listener)
@@ -554,17 +751,15 @@ class MediaDataManagerTest : SysuiTestCase() {
    @Test
    @Test
    fun testOnNotificationRemoved_blankTitle_notConverted() {
    fun testOnNotificationRemoved_blankTitle_notConverted() {
        // GIVEN that the manager has a notification with a resume action and blank title.
        // GIVEN that the manager has a notification with a resume action and blank title.
        whenever(controller.metadata)
            .thenReturn(
                metadataBuilder
                    .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
                    .build()
            )
        addNotificationAndLoad()
        addNotificationAndLoad()
        val data = mediaDataCaptor.value
        val data = mediaDataCaptor.value
        val instanceId = data.instanceId
        val instanceId = data.instanceId
        assertThat(data.resumption).isFalse()
        assertThat(data.resumption).isFalse()
        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
        mediaDataManager.onMediaDataLoaded(
            KEY,
            null,
            data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {})
        )


        // WHEN the notification is removed
        // WHEN the notification is removed
        reset(listener)
        reset(listener)