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

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

Merge "Time out resume controls" into sc-qpr1-dev am: 6ff8a0f6 am: 55aca3e1

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

Change-Id: I7805c00ffdd2dc4e6ef5bcff401cb5ac4d99127b
parents 4064c296 55aca3e1
Loading
Loading
Loading
Loading
+10 −6
Original line number Original line Diff line number Diff line
@@ -212,8 +212,8 @@ class MediaDataManager(
        mediaDataCombineLatest.addListener(mediaDataFilter)
        mediaDataCombineLatest.addListener(mediaDataFilter)


        // Set up links back into the pipeline for listeners that need to send events upstream.
        // Set up links back into the pipeline for listeners that need to send events upstream.
        mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
        mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
            setTimedOut(token, timedOut) }
            setTimedOut(key, timedOut) }
        mediaResumeListener.setManager(this)
        mediaResumeListener.setManager(this)
        mediaDataFilter.mediaDataManager = this
        mediaDataFilter.mediaDataManager = this


@@ -414,14 +414,18 @@ class MediaDataManager(
     * This will make the player not active anymore, hiding it from QQS and Keyguard.
     * This will make the player not active anymore, hiding it from QQS and Keyguard.
     * @see MediaData.active
     * @see MediaData.active
     */
     */
    internal fun setTimedOut(token: String, timedOut: Boolean, forceUpdate: Boolean = false) {
    internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
        mediaEntries[token]?.let {
        mediaEntries[key]?.let {
            if (it.active == !timedOut && !forceUpdate) {
            if (it.active == !timedOut && !forceUpdate) {
                if (it.resumption) {
                    if (DEBUG) Log.d(TAG, "timing out resume player $key")
                    dismissMediaData(key, 0L /* delay */)
                }
                return
                return
            }
            }
            it.active = !timedOut
            it.active = !timedOut
            if (DEBUG) Log.d(TAG, "Updating $token timedOut: $timedOut")
            if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
            onMediaDataLoaded(token, token, it)
            onMediaDataLoaded(key, key, it)
        }
        }
    }
    }


+39 −9
Original line number Original line Diff line number Diff line
@@ -35,6 +35,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.dump.DumpManager
import com.android.systemui.tuner.TunerService
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.Utils
import com.android.systemui.util.Utils
import com.android.systemui.util.time.SystemClock
import java.io.FileDescriptor
import java.io.FileDescriptor
import java.io.PrintWriter
import java.io.PrintWriter
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentLinkedQueue
@@ -53,11 +54,13 @@ class MediaResumeListener @Inject constructor(
    @Background private val backgroundExecutor: Executor,
    @Background private val backgroundExecutor: Executor,
    private val tunerService: TunerService,
    private val tunerService: TunerService,
    private val mediaBrowserFactory: ResumeMediaBrowserFactory,
    private val mediaBrowserFactory: ResumeMediaBrowserFactory,
    dumpManager: DumpManager
    dumpManager: DumpManager,
    private val systemClock: SystemClock
) : MediaDataManager.Listener, Dumpable {
) : MediaDataManager.Listener, Dumpable {


    private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
    private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
    private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
    private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
            ConcurrentLinkedQueue()


    private lateinit var mediaDataManager: MediaDataManager
    private lateinit var mediaDataManager: MediaDataManager


@@ -131,14 +134,32 @@ class MediaResumeListener @Inject constructor(
        val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
        val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
        val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
        val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
            ?.dropLastWhile { it.isEmpty() }
            ?.dropLastWhile { it.isEmpty() }
        var needsUpdate = false
        components?.forEach {
        components?.forEach {
            val info = it.split("/")
            val info = it.split("/")
            val packageName = info[0]
            val packageName = info[0]
            val className = info[1]
            val className = info[1]
            val component = ComponentName(packageName, className)
            val component = ComponentName(packageName, className)
            resumeComponents.add(component)

            val lastPlayed = if (info.size == 3) {
                try {
                    info[2].toLong()
                } catch (e: NumberFormatException) {
                    needsUpdate = true
                    systemClock.currentTimeMillis()
                }
            } else {
                needsUpdate = true
                systemClock.currentTimeMillis()
            }
            resumeComponents.add(component to lastPlayed)
        }
        }
        Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
        Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")

        if (needsUpdate) {
            // Save any missing times that we had to fill in
            writeSharedPrefs()
        }
    }
    }


    /**
    /**
@@ -149,11 +170,14 @@ class MediaResumeListener @Inject constructor(
            return
            return
        }
        }


        val now = systemClock.currentTimeMillis()
        resumeComponents.forEach {
        resumeComponents.forEach {
            val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
            if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
                val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first)
                browser.findRecentMedia()
                browser.findRecentMedia()
            }
            }
        }
        }
    }


    override fun onMediaDataLoaded(
    override fun onMediaDataLoaded(
        key: String,
        key: String,
@@ -234,18 +258,24 @@ class MediaResumeListener @Inject constructor(
     */
     */
    private fun updateResumptionList(componentName: ComponentName) {
    private fun updateResumptionList(componentName: ComponentName) {
        // Remove if exists
        // Remove if exists
        resumeComponents.remove(componentName)
        resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
        // Insert at front of queue
        // Insert at front of queue
        resumeComponents.add(componentName)
        val currentTime = systemClock.currentTimeMillis()
        resumeComponents.add(componentName to currentTime)
        // Remove old components if over the limit
        // Remove old components if over the limit
        if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
        if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
            resumeComponents.remove()
            resumeComponents.remove()
        }
        }


        // Save changes
        writeSharedPrefs()
    }

    private fun writeSharedPrefs() {
        val sb = StringBuilder()
        val sb = StringBuilder()
        resumeComponents.forEach {
        resumeComponents.forEach {
            sb.append(it.flattenToString())
            sb.append(it.first.flattenToString())
            sb.append("/")
            sb.append(it.second)
            sb.append(ResumeMediaBrowser.DELIMITER)
            sb.append(ResumeMediaBrowser.DELIMITER)
        }
        }
        val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
        val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+35 −11
Original line number Original line Diff line number Diff line
@@ -20,6 +20,7 @@ import android.media.session.MediaController
import android.media.session.PlaybackState
import android.media.session.PlaybackState
import android.os.SystemProperties
import android.os.SystemProperties
import android.util.Log
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
@@ -29,9 +30,15 @@ import javax.inject.Inject


private const val DEBUG = true
private const val DEBUG = true
private const val TAG = "MediaTimeout"
private const val TAG = "MediaTimeout"
private val PAUSED_MEDIA_TIMEOUT = SystemProperties

@VisibleForTesting
val PAUSED_MEDIA_TIMEOUT = SystemProperties
        .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
        .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))


@VisibleForTesting
val RESUME_MEDIA_TIMEOUT = SystemProperties
        .getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))

/**
/**
 * Controller responsible for keeping track of playback states and expiring inactive streams.
 * Controller responsible for keeping track of playback states and expiring inactive streams.
 */
 */
@@ -45,8 +52,9 @@ class MediaTimeoutListener @Inject constructor(


    /**
    /**
     * Callback representing that a media object is now expired:
     * Callback representing that a media object is now expired:
     * @param token Media session unique identifier
     * @param key Media control unique identifier
     * @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT}
     * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media,
     *                 or {@code RESUME_MEDIA_TIMEOUT} for resume media
     */
     */
    lateinit var timeoutCallback: (String, Boolean) -> Unit
    lateinit var timeoutCallback: (String, Boolean) -> Unit


@@ -122,6 +130,7 @@ class MediaTimeoutListener @Inject constructor(


        var timedOut = false
        var timedOut = false
        var playing: Boolean? = null
        var playing: Boolean? = null
        var resumption: Boolean? = null
        var destroyed = false
        var destroyed = false


        var mediaData: MediaData = data
        var mediaData: MediaData = data
@@ -159,13 +168,20 @@ class MediaTimeoutListener @Inject constructor(
        }
        }


        override fun onSessionDestroyed() {
        override fun onSessionDestroyed() {
            // If the session is destroyed, the controller is no longer valid, and we will need to
            // recreate it if this key is updated later
            if (DEBUG) {
            if (DEBUG) {
                Log.d(TAG, "Session destroyed for $key")
                Log.d(TAG, "Session destroyed for $key")
            }
            }

            if (resumption == true) {
                // Some apps create a session when MBS is queried. We should unregister the
                // controller since it will no longer be valid, but don't cancel the timeout
                mediaController?.unregisterCallback(this)
            } else {
                // For active controls, if the session is destroyed, clean up everything since we
                // will need to recreate it if this key is updated later
                destroy()
                destroy()
            }
            }
        }


        private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
        private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
            if (DEBUG) {
            if (DEBUG) {
@@ -173,20 +189,28 @@ class MediaTimeoutListener @Inject constructor(
            }
            }


            val isPlaying = state != null && isPlayingState(state.state)
            val isPlaying = state != null && isPlayingState(state.state)
            if (playing == isPlaying && playing != null) {
            val resumptionChanged = resumption != mediaData.resumption
            if (playing == isPlaying && playing != null && !resumptionChanged) {
                return
                return
            }
            }
            playing = isPlaying
            playing = isPlaying
            resumption = mediaData.resumption


            if (!isPlaying) {
            if (!isPlaying) {
                if (DEBUG) {
                if (DEBUG) {
                    Log.v(TAG, "schedule timeout for $key")
                    Log.v(TAG, "schedule timeout for $key playing $isPlaying, $resumption")
                }
                }
                if (cancellation != null) {
                if (cancellation != null && !resumptionChanged) {
                    // if the media changed resume state, we'll need to adjust the timeout length
                    if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.")
                    if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.")
                    return
                    return
                }
                }
                expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state")
                expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption")
                val timeout = if (mediaData.resumption) {
                    RESUME_MEDIA_TIMEOUT
                } else {
                    PAUSED_MEDIA_TIMEOUT
                }
                cancellation = mainExecutor.executeDelayed({
                cancellation = mainExecutor.executeDelayed({
                    cancellation = null
                    cancellation = null
                    if (DEBUG) {
                    if (DEBUG) {
@@ -195,7 +219,7 @@ class MediaTimeoutListener @Inject constructor(
                    timedOut = true
                    timedOut = true
                    // this event is async, so it's safe even when `dispatchEvents` is false
                    // this event is async, so it's safe even when `dispatchEvents` is false
                    timeoutCallback(key, timedOut)
                    timeoutCallback(key, timedOut)
                }, PAUSED_MEDIA_TIMEOUT)
                }, timeout)
            } else {
            } else {
                expireMediaTimeout(key, "playback started - $state, $key")
                expireMediaTimeout(key, "playback started - $state, $key")
                timedOut = false
                timedOut = false
+20 −1
Original line number Original line Diff line number Diff line
@@ -163,7 +163,7 @@ class MediaDataManagerTest : SysuiTestCase() {
    }
    }


    @Test
    @Test
    fun testSetTimedOut_deactivatesMedia() {
    fun testSetTimedOut_active_deactivatesMedia() {
        val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
        val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
                appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
                appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
                actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
                actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
@@ -175,6 +175,25 @@ class MediaDataManagerTest : SysuiTestCase() {
        assertThat(data.active).isFalse()
        assertThat(data.active).isFalse()
    }
    }


    @Test
    fun testSetTimedOut_resume_dismissesMedia() {
        // WHEN resume controls are present, and time out
        val desc = MediaDescription.Builder().run {
            setTitle(SESSION_TITLE)
            build()
        }
        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
                APP_NAME, pendingIntent, PACKAGE_NAME)
        backgroundExecutor.runAllReady()
        foregroundExecutor.runAllReady()
        mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)

        // THEN it is removed and listeners are informed
        foregroundExecutor.advanceClockToLast()
        foregroundExecutor.runAllReady()
        verify(listener).onMediaDataRemoved(PACKAGE_NAME)
    }

    @Test
    @Test
    fun testLoadsMetadataOnBackground() {
    fun testLoadsMetadataOnBackground() {
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+110 −3
Original line number Original line Diff line number Diff line
@@ -91,10 +91,12 @@ class MediaResumeListenerTest : SysuiTestCase() {


    @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
    @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
    @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
    @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
    @Captor lateinit var componentCaptor: ArgumentCaptor<String>


    private lateinit var executor: FakeExecutor
    private lateinit var executor: FakeExecutor
    private lateinit var data: MediaData
    private lateinit var data: MediaData
    private lateinit var resumeListener: MediaResumeListener
    private lateinit var resumeListener: MediaResumeListener
    private val clock = FakeSystemClock()


    private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
    private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
        Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
        Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
@@ -122,9 +124,9 @@ class MediaResumeListenerTest : SysuiTestCase() {
        whenever(mockContext.packageManager).thenReturn(context.packageManager)
        whenever(mockContext.packageManager).thenReturn(context.packageManager)
        whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
        whenever(mockContext.contentResolver).thenReturn(context.contentResolver)


        executor = FakeExecutor(FakeSystemClock())
        executor = FakeExecutor(clock)
        resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
        resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
                tunerService, resumeBrowserFactory, dumpManager)
                tunerService, resumeBrowserFactory, dumpManager, clock)
        resumeListener.setManager(mediaDataManager)
        resumeListener.setManager(mediaDataManager)
        mediaDataManager.addListener(resumeListener)
        mediaDataManager.addListener(resumeListener)


@@ -163,7 +165,7 @@ class MediaResumeListenerTest : SysuiTestCase() {


        // When listener is created, we do NOT register a user change listener
        // When listener is created, we do NOT register a user change listener
        val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
        val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
                resumeBrowserFactory, dumpManager)
                resumeBrowserFactory, dumpManager, clock)
        listener.setManager(mediaDataManager)
        listener.setManager(mediaDataManager)
        verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
        verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
            any(), any(), any())
            any(), any(), any())
@@ -328,4 +330,109 @@ class MediaResumeListenerTest : SysuiTestCase() {
        // Then we call restart
        // Then we call restart
        verify(resumeBrowser).restart()
        verify(resumeBrowser).restart()
    }
    }

    @Test
    fun testOnUserUnlock_missingTime_saves() {
        val currentTime = clock.currentTimeMillis()

        // When resume components without a last played time are loaded
        testOnUserUnlock_loadsTracks()

        // Then we save an update with the current time
        verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
        componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
                ?.dropLastWhile { it.isEmpty() }.forEach {
            val result = it.split("/")
            assertThat(result.size).isEqualTo(3)
            assertThat(result[2].toLong()).isEqualTo(currentTime)
        }
        verify(sharedPrefsEditor, times(1)).apply()
    }

    @Test
    fun testLoadComponents_recentlyPlayed_adds() {
        // Set up browser to return successfully
        val description = MediaDescription.Builder().setTitle(TITLE).build()
        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
        whenever(resumeBrowser.token).thenReturn(token)
        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
            callbackCaptor.value.addTrack(description, component, resumeBrowser)
        }

        // Set up shared preferences to have a component with a recent lastplayed time
        val lastPlayed = clock.currentTimeMillis()
        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
                tunerService, resumeBrowserFactory, dumpManager, clock)
        resumeListener.setManager(mediaDataManager)
        mediaDataManager.addListener(resumeListener)

        // When we load a component that was played recently
        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
        resumeListener.userChangeReceiver.onReceive(mockContext, intent)

        // We add its resume controls
        verify(resumeBrowser, times(1)).findRecentMedia()
        verify(mediaDataManager, times(1)).addResumptionControls(anyInt(),
                any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
    }

    @Test
    fun testLoadComponents_old_ignores() {
        // Set up shared preferences to have a component with an old lastplayed time
        val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
                tunerService, resumeBrowserFactory, dumpManager, clock)
        resumeListener.setManager(mediaDataManager)
        mediaDataManager.addListener(resumeListener)

        // When we load a component that is not recent
        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
        resumeListener.userChangeReceiver.onReceive(mockContext, intent)

        // We do not try to add resume controls
        verify(resumeBrowser, times(0)).findRecentMedia()
        verify(mediaDataManager, times(0)).addResumptionControls(anyInt(),
                any(), any(), any(), any(), any(), any())
    }

    @Test
    fun testOnLoad_hasService_updatesLastPlayed() {
        // Set up browser to return successfully
        val description = MediaDescription.Builder().setTitle(TITLE).build()
        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
        whenever(resumeBrowser.token).thenReturn(token)
        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
            callbackCaptor.value.addTrack(description, component, resumeBrowser)
        }

        // Set up shared preferences to have a component with a lastplayed time
        val currentTime = clock.currentTimeMillis()
        val lastPlayed = currentTime - 1000
        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
                tunerService, resumeBrowserFactory, dumpManager, clock)
        resumeListener.setManager(mediaDataManager)
        mediaDataManager.addListener(resumeListener)

        // When media data is loaded that has not been checked yet, and does have a MBS
        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)

        // Then we store the new lastPlayed time
        verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
        componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
                ?.dropLastWhile { it.isEmpty() }.forEach {
                    val result = it.split("/")
                    assertThat(result.size).isEqualTo(3)
                    assertThat(result[2].toLong()).isEqualTo(currentTime)
                }
        verify(sharedPrefsEditor, times(1)).apply()
    }
}
}
 No newline at end of file
Loading