Merge "Time out resume controls" into sc-qpr1-dev am: 6ff8a0f692
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15677510 Change-Id: I759d5787674a428ab85c5088df5a5637e0403f32
This commit is contained in:
@@ -212,8 +212,8 @@ class MediaDataManager(
|
||||
mediaDataCombineLatest.addListener(mediaDataFilter)
|
||||
|
||||
// Set up links back into the pipeline for listeners that need to send events upstream.
|
||||
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
|
||||
setTimedOut(token, timedOut) }
|
||||
mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
|
||||
setTimedOut(key, timedOut) }
|
||||
mediaResumeListener.setManager(this)
|
||||
mediaDataFilter.mediaDataManager = this
|
||||
|
||||
@@ -414,14 +414,18 @@ 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, forceUpdate: Boolean = false) {
|
||||
mediaEntries[token]?.let {
|
||||
internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
|
||||
mediaEntries[key]?.let {
|
||||
if (it.active == !timedOut && !forceUpdate) {
|
||||
if (it.resumption) {
|
||||
if (DEBUG) Log.d(TAG, "timing out resume player $key")
|
||||
dismissMediaData(key, 0L /* delay */)
|
||||
}
|
||||
return
|
||||
}
|
||||
it.active = !timedOut
|
||||
if (DEBUG) Log.d(TAG, "Updating $token timedOut: $timedOut")
|
||||
onMediaDataLoaded(token, token, it)
|
||||
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
|
||||
onMediaDataLoaded(key, key, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.tuner.TunerService
|
||||
import com.android.systemui.util.Utils
|
||||
import com.android.systemui.util.time.SystemClock
|
||||
import java.io.FileDescriptor
|
||||
import java.io.PrintWriter
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
@@ -53,11 +54,13 @@ class MediaResumeListener @Inject constructor(
|
||||
@Background private val backgroundExecutor: Executor,
|
||||
private val tunerService: TunerService,
|
||||
private val mediaBrowserFactory: ResumeMediaBrowserFactory,
|
||||
dumpManager: DumpManager
|
||||
dumpManager: DumpManager,
|
||||
private val systemClock: SystemClock
|
||||
) : MediaDataManager.Listener, Dumpable {
|
||||
|
||||
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
|
||||
|
||||
@@ -131,14 +134,32 @@ class MediaResumeListener @Inject constructor(
|
||||
val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
|
||||
val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
|
||||
?.dropLastWhile { it.isEmpty() }
|
||||
var needsUpdate = false
|
||||
components?.forEach {
|
||||
val info = it.split("/")
|
||||
val packageName = info[0]
|
||||
val className = info[1]
|
||||
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()}")
|
||||
|
||||
if (needsUpdate) {
|
||||
// Save any missing times that we had to fill in
|
||||
writeSharedPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,9 +170,12 @@ class MediaResumeListener @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
val now = systemClock.currentTimeMillis()
|
||||
resumeComponents.forEach {
|
||||
val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
|
||||
browser.findRecentMedia()
|
||||
if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
|
||||
val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first)
|
||||
browser.findRecentMedia()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,18 +258,24 @@ class MediaResumeListener @Inject constructor(
|
||||
*/
|
||||
private fun updateResumptionList(componentName: ComponentName) {
|
||||
// Remove if exists
|
||||
resumeComponents.remove(componentName)
|
||||
resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
|
||||
// Insert at front of queue
|
||||
resumeComponents.add(componentName)
|
||||
val currentTime = systemClock.currentTimeMillis()
|
||||
resumeComponents.add(componentName to currentTime)
|
||||
// Remove old components if over the limit
|
||||
if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
|
||||
resumeComponents.remove()
|
||||
}
|
||||
|
||||
// Save changes
|
||||
writeSharedPrefs()
|
||||
}
|
||||
|
||||
private fun writeSharedPrefs() {
|
||||
val sb = StringBuilder()
|
||||
resumeComponents.forEach {
|
||||
sb.append(it.flattenToString())
|
||||
sb.append(it.first.flattenToString())
|
||||
sb.append("/")
|
||||
sb.append(it.second)
|
||||
sb.append(ResumeMediaBrowser.DELIMITER)
|
||||
}
|
||||
val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.media.session.MediaController
|
||||
import android.media.session.PlaybackState
|
||||
import android.os.SystemProperties
|
||||
import android.util.Log
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
|
||||
@@ -29,9 +30,15 @@ import javax.inject.Inject
|
||||
|
||||
private const val DEBUG = true
|
||||
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))
|
||||
|
||||
@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.
|
||||
*/
|
||||
@@ -45,8 +52,9 @@ class MediaTimeoutListener @Inject constructor(
|
||||
|
||||
/**
|
||||
* Callback representing that a media object is now expired:
|
||||
* @param token Media session unique identifier
|
||||
* @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT}
|
||||
* @param key Media control unique identifier
|
||||
* @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
|
||||
|
||||
@@ -122,6 +130,7 @@ class MediaTimeoutListener @Inject constructor(
|
||||
|
||||
var timedOut = false
|
||||
var playing: Boolean? = null
|
||||
var resumption: Boolean? = null
|
||||
var destroyed = false
|
||||
|
||||
var mediaData: MediaData = data
|
||||
@@ -159,12 +168,19 @@ class MediaTimeoutListener @Inject constructor(
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@@ -173,20 +189,28 @@ class MediaTimeoutListener @Inject constructor(
|
||||
}
|
||||
|
||||
val isPlaying = state != null && isPlayingState(state.state)
|
||||
if (playing == isPlaying && playing != null) {
|
||||
val resumptionChanged = resumption != mediaData.resumption
|
||||
if (playing == isPlaying && playing != null && !resumptionChanged) {
|
||||
return
|
||||
}
|
||||
playing = isPlaying
|
||||
resumption = mediaData.resumption
|
||||
|
||||
if (!isPlaying) {
|
||||
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.")
|
||||
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 = null
|
||||
if (DEBUG) {
|
||||
@@ -195,7 +219,7 @@ class MediaTimeoutListener @Inject constructor(
|
||||
timedOut = true
|
||||
// this event is async, so it's safe even when `dispatchEvents` is false
|
||||
timeoutCallback(key, timedOut)
|
||||
}, PAUSED_MEDIA_TIMEOUT)
|
||||
}, timeout)
|
||||
} else {
|
||||
expireMediaTimeout(key, "playback started - $state, $key")
|
||||
timedOut = false
|
||||
|
||||
@@ -163,7 +163,7 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetTimedOut_deactivatesMedia() {
|
||||
fun testSetTimedOut_active_deactivatesMedia() {
|
||||
val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
|
||||
appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
|
||||
actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
|
||||
@@ -175,6 +175,25 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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
|
||||
fun testLoadsMetadataOnBackground() {
|
||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
||||
|
||||
@@ -91,10 +91,12 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
|
||||
@Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
|
||||
@Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
|
||||
@Captor lateinit var componentCaptor: ArgumentCaptor<String>
|
||||
|
||||
private lateinit var executor: FakeExecutor
|
||||
private lateinit var data: MediaData
|
||||
private lateinit var resumeListener: MediaResumeListener
|
||||
private val clock = FakeSystemClock()
|
||||
|
||||
private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
|
||||
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
|
||||
@@ -122,9 +124,9 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
whenever(mockContext.packageManager).thenReturn(context.packageManager)
|
||||
whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
|
||||
|
||||
executor = FakeExecutor(FakeSystemClock())
|
||||
executor = FakeExecutor(clock)
|
||||
resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
|
||||
tunerService, resumeBrowserFactory, dumpManager)
|
||||
tunerService, resumeBrowserFactory, dumpManager, clock)
|
||||
resumeListener.setManager(mediaDataManager)
|
||||
mediaDataManager.addListener(resumeListener)
|
||||
|
||||
@@ -163,7 +165,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
|
||||
// When listener is created, we do NOT register a user change listener
|
||||
val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
|
||||
resumeBrowserFactory, dumpManager)
|
||||
resumeBrowserFactory, dumpManager, clock)
|
||||
listener.setManager(mediaDataManager)
|
||||
verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
|
||||
any(), any(), any())
|
||||
@@ -328,4 +330,109 @@ class MediaResumeListenerTest : SysuiTestCase() {
|
||||
// Then we call 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()
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
private lateinit var playbackBuilder: PlaybackState.Builder
|
||||
private lateinit var session: MediaSession
|
||||
private lateinit var mediaData: MediaData
|
||||
private lateinit var resumeData: MediaData
|
||||
private lateinit var mediaTimeoutListener: MediaTimeoutListener
|
||||
|
||||
@Before
|
||||
@@ -97,6 +98,10 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||
emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = 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
|
||||
@@ -120,6 +125,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
|
||||
assertThat(executor.numPending()).isEqualTo(1)
|
||||
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
|
||||
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,6 +194,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
|
||||
assertThat(executor.numPending()).isEqualTo(1)
|
||||
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -229,7 +236,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnSessionDestroyed_clearsTimeout() {
|
||||
fun testOnSessionDestroyed_active_clearsTimeout() {
|
||||
// GIVEN media that is paused
|
||||
val mediaPaused = mediaData.copy(isPlaying = false)
|
||||
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
|
||||
@@ -247,7 +254,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testSessionDestroyed_thenRestarts_resetsTimeout() {
|
||||
// Assuming we have previously destroyed the session
|
||||
testOnSessionDestroyed_clearsTimeout()
|
||||
testOnSessionDestroyed_active_clearsTimeout()
|
||||
|
||||
// WHEN we get an update with media playing
|
||||
val playingState = mock(android.media.session.PlaybackState::class.java)
|
||||
@@ -264,4 +271,91 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user