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

Commit 5529e39a authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Android (Google) Code Review
Browse files

Merge "Show recently active media on smartspace signal" into sc-dev

parents 0e7cd4bd 33970685
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -104,7 +104,12 @@ data class MediaData(
    /**
     * Set from the notification and used as fallback when PlaybackState cannot be determined
     */
    val isClearable: Boolean = true
    val isClearable: Boolean = true,

    /**
     * Timestamp when this player was last active.
     */
    var lastActive: Long = 0L
)

/** State of a media action. */
+53 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.media

import android.app.smartspace.SmartspaceTarget
import android.os.SystemProperties
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -24,14 +25,23 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.CurrentUserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import javax.inject.Inject

private const val TAG = "MediaDataFilter"
private const val DEBUG = true

/**
 * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
 * available within this time window, smartspace recommendations will be shown instead.
 */
private val SMARTSPACE_MAX_AGE = SystemProperties
        .getLong("debug.sysui.smartspace_max_age", TimeUnit.HOURS.toMillis(3))

/**
 * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
 * switches (removing entries for the previous user, adding back entries for the current user)
 * switches (removing entries for the previous user, adding back entries for the current user). Also
 * filters out smartspace updates in favor of local recent media, when avaialble.
 *
 * This is added at the end of the pipeline since we may still need to handle callbacks from
 * background users (e.g. timeouts).
@@ -52,6 +62,7 @@ class MediaDataFilter @Inject constructor(
    // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
    private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
    private var hasSmartspace: Boolean = false
    private var reactivatedKey: String? = null

    init {
        userTracker = object : CurrentUserTracker(broadcastDispatcher) {
@@ -86,6 +97,30 @@ class MediaDataFilter @Inject constructor(

    override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
        hasSmartspace = true

        // Before forwarding the smartspace target, first check if we have recently inactive media
        val now = System.currentTimeMillis()
        val sorted = userEntries.toSortedMap(compareBy {
            userEntries.get(it)?.lastActive ?: -1
        })
        if (sorted.size > 0) {
            val lastActiveKey = sorted.lastKey() // most recently active
            val timeSinceActive = sorted.get(lastActiveKey)?.let {
                now - it.lastActive
            } ?: Long.MAX_VALUE
            if (timeSinceActive < SMARTSPACE_MAX_AGE) {
                // Notify listeners to consider this media active
                Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
                reactivatedKey = lastActiveKey
                val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
                listeners.forEach {
                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData)
                }
                return
            }
        }

        // If no recent media, continue with smartspace update
        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data) }
    }

@@ -101,6 +136,21 @@ class MediaDataFilter @Inject constructor(

    override fun onSmartspaceMediaDataRemoved(key: String) {
        hasSmartspace = false

        // First check if we had reactivated media instead of forwarding smartspace
        reactivatedKey?.let {
            val lastActiveKey = it
            reactivatedKey = null
            Log.d(TAG, "expiring reactivated key $lastActiveKey")
            // Notify listeners to update with actual active value
            userEntries.get(lastActiveKey)?.let { mediaData ->
                listeners.forEach {
                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData)
                }
            }
            return
        }

        listeners.forEach { it.onSmartspaceMediaDataRemoved(key) }
    }

@@ -137,7 +187,8 @@ class MediaDataFilter @Inject constructor(
        if (DEBUG) Log.d(TAG, "Media carousel swiped away")
        val mediaKeys = userEntries.keys.toSet()
        mediaKeys.forEach {
            mediaDataManager.setTimedOut(it, timedOut = true)
            // Force updates to listeners, needed for re-activated card
            mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
        }
        if (hasSmartspace) {
            mediaDataManager.dismissSmartspaceRecommendation()
+7 −5
Original line number Diff line number Diff line
@@ -394,9 +394,9 @@ class MediaDataManager(
     * This will make the player not active anymore, hiding it from QQS and Keyguard.
     * @see MediaData.active
     */
    internal fun setTimedOut(token: String, timedOut: Boolean) {
    internal fun setTimedOut(token: String, timedOut: Boolean, forceUpdate: Boolean = false) {
        mediaEntries[token]?.let {
            if (it.active == !timedOut) {
            if (it.active == !timedOut && !forceUpdate) {
                return
            }
            it.active = !timedOut
@@ -470,12 +470,13 @@ class MediaDataManager(
        }

        val mediaAction = getResumeMediaAction(resumeAction)
        val lastActive = System.currentTimeMillis()
        foregroundExecutor.execute {
            onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName,
                    null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
                    packageName, token, appIntent, device = null, active = false,
                    resumeAction = resumeAction, resumption = true, notificationKey = packageName,
                    hasCheckedForResume = true))
                    hasCheckedForResume = true, lastActive = lastActive))
        }
    }

@@ -588,7 +589,7 @@ class MediaDataManager(
        val isLocalSession = mediaController.playbackInfo?.playbackType ==
            MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ?: true
        val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null

        val lastActive = System.currentTimeMillis()
        foregroundExecutor.execute {
            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
@@ -598,7 +599,8 @@ class MediaDataManager(
                    actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null,
                    active, resumeAction = resumeAction, isLocalSession = isLocalSession,
                    notificationKey = key, hasCheckedForResume = hasCheckedForResume,
                    isPlaying = isPlaying, isClearable = sbn.isClearable()))
                    isPlaying = isPlaying, isClearable = sbn.isClearable(),
                    lastActive = lastActive))
        }
    }

+1 −1
Original line number Diff line number Diff line
@@ -74,7 +74,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {

        mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
                new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true,
                false, KEY, false, false, false);
                false, KEY, false, false, false, 0L);
        mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
    }

+62 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.media

import android.app.smartspace.SmartspaceTarget
import android.graphics.Color
import androidx.test.filters.SmallTest
import android.testing.AndroidTestingRunner
@@ -47,6 +48,7 @@ private const val PACKAGE = "PKG"
private const val ARTIST = "ARTIST"
private const val TITLE = "TITLE"
private const val DEVICE_NAME = "DEVICE_NAME"
private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"

private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
private fun <T> any(): T = Mockito.any()
@@ -68,6 +70,8 @@ class MediaDataFilterTest : SysuiTestCase() {
    private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
    @Mock
    private lateinit var executor: Executor
    @Mock
    private lateinit var smartspaceData: SmartspaceTarget

    private lateinit var mediaDataFilter: MediaDataFilter
    private lateinit var dataMain: MediaData
@@ -91,6 +95,8 @@ class MediaDataFilterTest : SysuiTestCase() {

        dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
            emptyList(), emptyList(), PACKAGE, null, null, device, true, null)

        `when`(smartspaceData.smartspaceTargetId).thenReturn(SMARTSPACE_KEY)
    }

    private fun setUser(id: Int) {
@@ -212,6 +218,61 @@ class MediaDataFilterTest : SysuiTestCase() {
        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
        mediaDataFilter.onSwipeToDismiss()

        verify(mediaDataManager).setTimedOut(eq(KEY), eq(true))
        verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true))
    }

    @Test
    fun testOnSmartspaceMediaDataLoaded_noMedia_usesSmartspace() {
        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)

        verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData))
        assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
    }

    @Test
    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_usesSmartspace() {
        val dataOld = dataMain.copy(active = false, lastActive = 0L)
        mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)

        verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData))
        assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
    }

    @Test
    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_usesMedia() {
        // WHEN we have media that was recently played, but not currently active
        val dataCurrent = dataMain.copy(active = false, lastActive = System.currentTimeMillis())
        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent))

        // AND we get a smartspace signal
        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)

        // THEN we should tell listeners to treat the media as active instead
        val dataCurrentAndActive = dataCurrent.copy(active = true)
        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive))
        assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
    }

    @Test
    fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsMedia() {
        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
        mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)

        verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
    }

    @Test
    fun testOnSmartspaceMediaDataRemoved_usedMedia_clearsMedia() {
        val dataCurrent = dataMain.copy(active = false, lastActive = System.currentTimeMillis())
        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)

        mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)

        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrent))
        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
    }
}
Loading