Merge "Time out resume controls" into sc-qpr1-dev

This commit is contained in:
Beth Thibodeau
2021-09-09 16:27:35 +00:00
committed by Android (Google) Code Review
6 changed files with 310 additions and 32 deletions

View File

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

View File

@@ -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,9 +170,12 @@ 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) {
browser.findRecentMedia() val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first)
browser.findRecentMedia()
}
} }
} }
@@ -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)

View File

@@ -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,12 +168,19 @@ 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")
} }
destroy()
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()
}
} }
private fun processState(state: PlaybackState?, dispatchEvents: Boolean) { private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
@@ -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

View File

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

View File

@@ -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()
}
} }

View File

@@ -71,6 +71,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
private lateinit var playbackBuilder: PlaybackState.Builder private lateinit var playbackBuilder: PlaybackState.Builder
private lateinit var session: MediaSession private lateinit var session: MediaSession
private lateinit var mediaData: MediaData private lateinit var mediaData: MediaData
private lateinit var resumeData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener private lateinit var mediaTimeoutListener: MediaTimeoutListener
@Before @Before
@@ -97,6 +98,10 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null,
device = null, active = true, resumeAction = null) device = null, active = true, resumeAction = null)
resumeData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
emptyList(), emptyList(), PACKAGE, null, clickIntent = null,
device = null, active = false, resumeAction = null, resumption = true)
} }
@Test @Test
@@ -120,6 +125,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
} }
@Test @Test
@@ -188,6 +194,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
} }
@Test @Test
@@ -229,7 +236,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
} }
@Test @Test
fun testOnSessionDestroyed_clearsTimeout() { fun testOnSessionDestroyed_active_clearsTimeout() {
// GIVEN media that is paused // GIVEN media that is paused
val mediaPaused = mediaData.copy(isPlaying = false) val mediaPaused = mediaData.copy(isPlaying = false)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
@@ -247,7 +254,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testSessionDestroyed_thenRestarts_resetsTimeout() { fun testSessionDestroyed_thenRestarts_resetsTimeout() {
// Assuming we have previously destroyed the session // Assuming we have previously destroyed the session
testOnSessionDestroyed_clearsTimeout() testOnSessionDestroyed_active_clearsTimeout()
// WHEN we get an update with media playing // WHEN we get an update with media playing
val playingState = mock(android.media.session.PlaybackState::class.java) val playingState = mock(android.media.session.PlaybackState::class.java)
@@ -264,4 +271,91 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
} }
verify(timeoutCallback).invoke(eq(KEY), eq(false)) verify(timeoutCallback).invoke(eq(KEY), eq(false))
} }
@Test
fun testOnSessionDestroyed_resume_continuesTimeout() {
// GIVEN resume media with session info
val resumeWithSession = resumeData.copy(token = session.sessionToken)
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
assertThat(executor.numPending()).isEqualTo(1)
// WHEN the session is destroyed
mediaCallbackCaptor.value.onSessionDestroyed()
// THEN the controller is unregistered, but the timeout is still scheduled
verify(mediaController).unregisterCallback(anyObject())
assertThat(executor.numPending()).isEqualTo(1)
}
@Test
fun testOnMediaDataLoaded_activeToResume_registersTimeout() {
// WHEN a regular media is loaded
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
// AND it turns into a resume control
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
// THEN we register a timeout
assertThat(executor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
// WHEN regular media is paused
val pausedState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
`when`(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1)
// AND it turns into a resume control
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
// THEN we update the timeout length
assertThat(executor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataLoaded_resumption_registersTimeout() {
// WHEN a resume media is loaded
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
// THEN we register a timeout
assertThat(executor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() {
// WHEN we have a resume control
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
// AND that media is resumed
val playingState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
`when`(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
// THEN the timeout length is changed to a regular media control
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataRemoved_resume_timeoutCancelled() {
// WHEN we have a resume control
testOnMediaDataLoaded_resumption_registersTimeout()
// AND the media is removed
mediaTimeoutListener.onMediaDataRemoved(PACKAGE)
// THEN the timeout runnable is cancelled
assertThat(executor.numPending()).isEqualTo(0)
}
} }