Merge "Move media expiration to MediaDataManager" into rvc-dev am: 61ee50f7ae
Change-Id: Id3081b6deb390efe4ecec7b56b0d4703aea38e9d
This commit is contained in:
@@ -35,7 +35,8 @@ data class MediaData(
|
||||
val packageName: String?,
|
||||
val token: MediaSession.Token?,
|
||||
val clickIntent: PendingIntent?,
|
||||
val device: MediaDeviceData?
|
||||
val device: MediaDeviceData?,
|
||||
val notificationKey: String = "INVALID"
|
||||
)
|
||||
|
||||
/** State of a media action. */
|
||||
|
||||
@@ -35,6 +35,8 @@ import com.android.internal.graphics.ColorUtils
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.statusbar.notification.MediaNotificationProcessor
|
||||
import com.android.systemui.statusbar.notification.NotificationEntryManager
|
||||
import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON
|
||||
import com.android.systemui.statusbar.notification.row.HybridGroupManager
|
||||
import com.android.systemui.util.Assert
|
||||
import com.android.systemui.util.Utils
|
||||
@@ -77,6 +79,8 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||
class MediaDataManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val mediaControllerFactory: MediaControllerFactory,
|
||||
private val mediaTimeoutListener: MediaTimeoutListener,
|
||||
private val notificationEntryManager: NotificationEntryManager,
|
||||
@Background private val backgroundExecutor: Executor,
|
||||
@Main private val foregroundExecutor: Executor
|
||||
) {
|
||||
@@ -84,6 +88,12 @@ class MediaDataManager @Inject constructor(
|
||||
private val listeners: MutableSet<Listener> = mutableSetOf()
|
||||
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||
|
||||
init {
|
||||
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
|
||||
setTimedOut(token, timedOut) }
|
||||
addListener(mediaTimeoutListener)
|
||||
}
|
||||
|
||||
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
||||
if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
|
||||
Assert.isMainThread()
|
||||
@@ -112,6 +122,16 @@ class MediaDataManager @Inject constructor(
|
||||
*/
|
||||
fun removeListener(listener: Listener) = listeners.remove(listener)
|
||||
|
||||
private fun setTimedOut(token: String, timedOut: Boolean) {
|
||||
if (!timedOut) {
|
||||
return
|
||||
}
|
||||
mediaEntries[token]?.let {
|
||||
notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */,
|
||||
UNDEFINED_DISMISS_REASON)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
|
||||
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
|
||||
as MediaSession.Token?
|
||||
@@ -223,7 +243,7 @@ class MediaDataManager @Inject constructor(
|
||||
foregroundExecutor.execute {
|
||||
onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
|
||||
artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
||||
notif.contentIntent, null))
|
||||
notif.contentIntent, null, key))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.media.session.MediaController
|
||||
import android.media.session.PlaybackState
|
||||
import android.os.SystemProperties
|
||||
import android.util.Log
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val DEBUG = true
|
||||
private const val TAG = "MediaTimeout"
|
||||
private val PAUSED_MEDIA_TIMEOUT = SystemProperties
|
||||
.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
|
||||
|
||||
/**
|
||||
* Controller responsible for keeping track of playback states and expiring inactive streams.
|
||||
*/
|
||||
@Singleton
|
||||
class MediaTimeoutListener @Inject constructor(
|
||||
private val mediaControllerFactory: MediaControllerFactory,
|
||||
@Main private val mainExecutor: DelayableExecutor
|
||||
) : MediaDataManager.Listener {
|
||||
|
||||
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
|
||||
|
||||
lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||
|
||||
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
||||
if (mediaListeners.containsKey(key)) {
|
||||
return
|
||||
}
|
||||
mediaListeners[key] = PlaybackStateListener(key, data)
|
||||
}
|
||||
|
||||
override fun onMediaDataRemoved(key: String) {
|
||||
mediaListeners.remove(key)?.destroy()
|
||||
}
|
||||
|
||||
fun isTimedOut(key: String): Boolean {
|
||||
return mediaListeners[key]?.timedOut ?: false
|
||||
}
|
||||
|
||||
private inner class PlaybackStateListener(
|
||||
private val key: String,
|
||||
data: MediaData
|
||||
) : MediaController.Callback() {
|
||||
|
||||
var timedOut = false
|
||||
|
||||
private val mediaController = mediaControllerFactory.create(data.token)
|
||||
private var cancellation: Runnable? = null
|
||||
|
||||
init {
|
||||
mediaController.registerCallback(this)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
mediaController.unregisterCallback(this)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(state: PlaybackState?) {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "onPlaybackStateChanged: $state")
|
||||
}
|
||||
expireMediaTimeout(key, "playback state ativity - $state, $key")
|
||||
|
||||
if (state == null || !isPlayingState(state.state)) {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "schedule timeout for $key")
|
||||
}
|
||||
expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state")
|
||||
cancellation = mainExecutor.executeDelayed({
|
||||
cancellation = null
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Execute timeout for $key")
|
||||
}
|
||||
timedOut = true
|
||||
timeoutCallback(key, timedOut)
|
||||
}, PAUSED_MEDIA_TIMEOUT)
|
||||
} else {
|
||||
timedOut = false
|
||||
timeoutCallback(key, timedOut)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expireMediaTimeout(mediaNotificationKey: String, reason: String) {
|
||||
cancellation?.apply {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG,
|
||||
"media timeout cancelled for $mediaNotificationKey, reason: $reason")
|
||||
}
|
||||
run()
|
||||
}
|
||||
cancellation = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.android.systemui.statusbar;
|
||||
|
||||
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
|
||||
import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON;
|
||||
import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_MEDIA_FAKE_ARTWORK;
|
||||
import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_WALLPAPER;
|
||||
import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK;
|
||||
@@ -36,7 +35,6 @@ import android.media.session.MediaSession;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.SystemProperties;
|
||||
import android.os.Trace;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.DeviceConfig;
|
||||
@@ -80,7 +78,6 @@ import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import dagger.Lazy;
|
||||
|
||||
@@ -91,8 +88,6 @@ import dagger.Lazy;
|
||||
public class NotificationMediaManager implements Dumpable {
|
||||
private static final String TAG = "NotificationMediaManager";
|
||||
public static final boolean DEBUG_MEDIA = false;
|
||||
private static final long PAUSED_MEDIA_TIMEOUT = SystemProperties
|
||||
.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10));
|
||||
|
||||
private final StatusBarStateController mStatusBarStateController
|
||||
= Dependency.get(StatusBarStateController.class);
|
||||
@@ -134,7 +129,6 @@ public class NotificationMediaManager implements Dumpable {
|
||||
private MediaController mMediaController;
|
||||
private String mMediaNotificationKey;
|
||||
private MediaMetadata mMediaMetadata;
|
||||
private Runnable mMediaTimeoutCancellation;
|
||||
|
||||
private BackDropView mBackdrop;
|
||||
private ImageView mBackdropFront;
|
||||
@@ -164,47 +158,11 @@ public class NotificationMediaManager implements Dumpable {
|
||||
if (DEBUG_MEDIA) {
|
||||
Log.v(TAG, "DEBUG_MEDIA: onPlaybackStateChanged: " + state);
|
||||
}
|
||||
if (mMediaTimeoutCancellation != null) {
|
||||
if (DEBUG_MEDIA) {
|
||||
Log.v(TAG, "DEBUG_MEDIA: media timeout cancelled");
|
||||
}
|
||||
mMediaTimeoutCancellation.run();
|
||||
mMediaTimeoutCancellation = null;
|
||||
}
|
||||
if (state != null) {
|
||||
if (!isPlaybackActive(state.getState())) {
|
||||
clearCurrentMediaNotification();
|
||||
}
|
||||
findAndUpdateMediaNotifications();
|
||||
scheduleMediaTimeout(state);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleMediaTimeout(PlaybackState state) {
|
||||
final NotificationEntry entry;
|
||||
synchronized (mEntryManager) {
|
||||
entry = mEntryManager.getActiveNotificationUnfiltered(mMediaNotificationKey);
|
||||
}
|
||||
if (entry != null) {
|
||||
if (!isPlayingState(state.getState())) {
|
||||
if (DEBUG_MEDIA) {
|
||||
Log.v(TAG, "DEBUG_MEDIA: schedule timeout for "
|
||||
+ mMediaNotificationKey);
|
||||
}
|
||||
mMediaTimeoutCancellation = mMainExecutor.executeDelayed(() -> {
|
||||
synchronized (mEntryManager) {
|
||||
if (DEBUG_MEDIA) {
|
||||
Log.v(TAG, "DEBUG_MEDIA: execute timeout for "
|
||||
+ mMediaNotificationKey);
|
||||
}
|
||||
if (mMediaNotificationKey == null) {
|
||||
return;
|
||||
}
|
||||
mEntryManager.removeNotification(mMediaNotificationKey, null,
|
||||
UNDEFINED_DISMISS_REASON);
|
||||
}
|
||||
}, PAUSED_MEDIA_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||
mManager.addListener(mListener);
|
||||
|
||||
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null);
|
||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, KEY);
|
||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.media.session.MediaController
|
||||
import android.media.session.PlaybackState
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.clearInvocations
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
|
||||
private const val KEY = "KEY"
|
||||
|
||||
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
private fun <T> anyObject(): T {
|
||||
return Mockito.anyObject<T>()
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class MediaTimeoutListenerTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
|
||||
@Mock private lateinit var mediaController: MediaController
|
||||
@Mock private lateinit var executor: DelayableExecutor
|
||||
@Mock private lateinit var mediaData: MediaData
|
||||
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||
@Mock private lateinit var cancellationRunnable: Runnable
|
||||
@Captor private lateinit var timeoutCaptor: ArgumentCaptor<Runnable>
|
||||
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||
private lateinit var mediaTimeoutListener: MediaTimeoutListener
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
|
||||
`when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable)
|
||||
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor)
|
||||
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnMediaDataLoaded_registersPlaybackListener() {
|
||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
||||
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
|
||||
|
||||
// Ignores is same key
|
||||
clearInvocations(mediaController)
|
||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
||||
verify(mediaController, never()).registerCallback(anyObject())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnMediaDataRemoved_unregistersPlaybackListener() {
|
||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
||||
mediaTimeoutListener.onMediaDataRemoved(KEY)
|
||||
verify(mediaController).unregisterCallback(anyObject())
|
||||
|
||||
// Ignores duplicate requests
|
||||
clearInvocations(mediaController)
|
||||
mediaTimeoutListener.onMediaDataRemoved(KEY)
|
||||
verify(mediaController, never()).unregisterCallback(anyObject())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_schedulesTimeout_whenPaused() {
|
||||
// Assuming we're registered
|
||||
testOnMediaDataLoaded_registersPlaybackListener()
|
||||
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
|
||||
verify(executor).executeDelayed(capture(timeoutCaptor), anyLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPlaybackStateChanged_cancelsTimeout_whenResumed() {
|
||||
// Assuming we're have a pending timeout
|
||||
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
|
||||
|
||||
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
|
||||
.setState(PlaybackState.STATE_PLAYING, 0L, 0f).build())
|
||||
verify(cancellationRunnable).run()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTimeoutCallback_invokedIfTimeout() {
|
||||
// Assuming we're have a pending timeout
|
||||
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
|
||||
|
||||
timeoutCaptor.value.run()
|
||||
verify(timeoutCallback).invoke(eq(KEY), eq(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsTimedOut() {
|
||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
||||
assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user