diff options
| author | 2020-06-01 17:28:06 +0000 | |
|---|---|---|
| committer | 2020-06-01 17:28:06 +0000 | |
| commit | 189fb115a008df2fdeb939fcdd2c79211c681df1 (patch) | |
| tree | bde577afd480e79e3318b557ae8a0571ce1a0107 | |
| parent | 8538ad40d242a0d1d11ab0459272503704cb3918 (diff) | |
| parent | 61ee50f7aea9aa57596cbd644efba6f4fc04fae1 (diff) | |
Merge "Move media expiration to MediaDataManager" into rvc-dev am: 61ee50f7ae
Change-Id: Id3081b6deb390efe4ecec7b56b0d4703aea38e9d
6 files changed, 271 insertions, 45 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 330a5c0dcad3..a94f6a87d58a 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 @@ 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. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index cf7fbfa9461e..d94985703083 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.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)) } } 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 000000000000..92a1ab1b1871 --- /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 db5329a8f952..217148df60e2 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.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); - } } } 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 aa889a64c1b8..48e3b0a9d993 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 @@ 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); } 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 000000000000..c21343cb5423 --- /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 |