Merge "Time out resume controls" into sc-qpr1-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
6ff8a0f692
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user