Merge "Move media expiration to MediaDataManager" into rvc-dev am: 61ee50f7ae
Change-Id: Id3081b6deb390efe4ecec7b56b0d4703aea38e9d
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index 330a5c0..a94f6a8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -35,7 +35,8 @@
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. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index cf7fbfa..d949857 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -35,6 +35,8 @@
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 @@
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 @@
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 @@
*/
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 @@
foregroundExecutor.execute {
onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
- notif.contentIntent, null))
+ notif.contentIntent, null, key))
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
new file mode 100644
index 0000000..92a1ab1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index db5329a..217148d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -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.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.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import dagger.Lazy;
@@ -91,8 +88,6 @@
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 @@
private MediaController mMediaController;
private String mMediaNotificationKey;
private MediaMetadata mMediaMetadata;
- private Runnable mMediaTimeoutCancellation;
private BackDropView mBackdrop;
private ImageView mBackdropFront;
@@ -164,47 +158,11 @@
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);
- }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
index aa889a6..48e3b0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
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);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
new file mode 100644
index 0000000..c21343c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
@@ -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()
+ }
+}
\ No newline at end of file