Loading packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +50 −19 Original line number Original line Diff line number Diff line Loading @@ -38,18 +38,20 @@ import android.service.notification.StatusBarNotification import android.text.TextUtils import android.text.TextUtils import android.util.Log import android.util.Log import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.ColorUtils import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.notification.MediaNotificationProcessor 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.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert import com.android.systemui.util.Assert import com.android.systemui.util.Utils import com.android.systemui.util.Utils import java.io.FileDescriptor import java.io.IOException import java.io.IOException import java.io.PrintWriter import java.util.concurrent.Executor import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Inject import javax.inject.Singleton import javax.inject.Singleton Loading Loading @@ -85,20 +87,35 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean { * A class that facilitates management and loading of Media Data, ready for binding. * A class that facilitates management and loading of Media Data, ready for binding. */ */ @Singleton @Singleton class MediaDataManager @Inject constructor( class MediaDataManager( private val context: Context, private val context: Context, private val mediaControllerFactory: MediaControllerFactory, private val notificationEntryManager: NotificationEntryManager, @Background private val backgroundExecutor: Executor, @Background private val backgroundExecutor: Executor, @Main private val foregroundExecutor: Executor, @Main private val foregroundExecutor: Executor, broadcastDispatcher: BroadcastDispatcher, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, dumpManager: DumpManager, mediaTimeoutListener: MediaTimeoutListener, mediaTimeoutListener: MediaTimeoutListener, mediaResumeListener: MediaResumeListener mediaResumeListener: MediaResumeListener, ) { private val useMediaResumption: Boolean, private val useQsMediaPlayer: Boolean ) : Dumpable { private val listeners: MutableSet<Listener> = mutableSetOf() private val listeners: MutableSet<Listener> = mutableSetOf() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() private val useMediaResumption: Boolean = Utils.useMediaResumption(context) @Inject constructor( context: Context, @Background backgroundExecutor: Executor, @Main foregroundExecutor: Executor, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, broadcastDispatcher: BroadcastDispatcher, mediaTimeoutListener: MediaTimeoutListener, mediaResumeListener: MediaResumeListener ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context)) private val userChangeReceiver = object : BroadcastReceiver() { private val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) { Loading Loading @@ -128,6 +145,7 @@ class MediaDataManager @Inject constructor( } } init { init { dumpManager.registerDumpable(TAG, this) mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> setTimedOut(token, timedOut) } setTimedOut(token, timedOut) } addListener(mediaTimeoutListener) addListener(mediaTimeoutListener) Loading Loading @@ -159,8 +177,13 @@ class MediaDataManager @Inject constructor( context.registerReceiver(appChangeReceiver, uninstallFilter) context.registerReceiver(appChangeReceiver, uninstallFilter) } } fun destroy() { context.unregisterReceiver(appChangeReceiver) broadcastDispatcher.unregisterReceiver(userChangeReceiver) } fun onNotificationAdded(key: String, sbn: StatusBarNotification) { fun onNotificationAdded(key: String, sbn: StatusBarNotification) { if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) { if (useQsMediaPlayer && isMediaNotification(sbn)) { Assert.isMainThread() Assert.isMainThread() val oldKey = findExistingEntry(key, sbn.packageName) val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { if (oldKey == null) { Loading Loading @@ -253,18 +276,18 @@ class MediaDataManager @Inject constructor( */ */ fun removeListener(listener: Listener) = listeners.remove(listener) fun removeListener(listener: Listener) = listeners.remove(listener) /** * Called whenever the player has been paused or stopped for a while. * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ private fun setTimedOut(token: String, timedOut: Boolean) { private fun setTimedOut(token: String, timedOut: Boolean) { mediaEntries[token]?.let { mediaEntries[token]?.let { if (Utils.useMediaResumption(context)) { if (it.active == !timedOut) { if (it.active == !timedOut) { return return } } it.active = !timedOut it.active = !timedOut onMediaDataLoaded(token, token, it) onMediaDataLoaded(token, token, it) } else if (timedOut) { notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */, UNDEFINED_DISMISS_REASON) } } } } } Loading Loading @@ -570,4 +593,12 @@ class MediaDataManager @Inject constructor( */ */ fun onMediaDataRemoved(key: String) {} fun onMediaDataRemoved(key: String) {} } } override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { pw.apply { println("listeners: $listeners") println("mediaEntries: $mediaEntries") println("useMediaResumption: $useMediaResumption") } } } } packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt 0 → 100644 +126 −0 Original line number Original line Diff line number Diff line package com.android.systemui.media import android.app.Notification import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import java.util.concurrent.Executor import org.mockito.Mockito.`when` as whenever private const val KEY = "KEY" private const val PACKAGE_NAME = "com.android.systemui" private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> anyObject(): T { return Mockito.anyObject<T>() } @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidTestingRunner::class) class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var mediaControllerFactory: MediaControllerFactory @Mock lateinit var backgroundExecutor: Executor @Mock lateinit var foregroundExecutor: Executor @Mock lateinit var dumpManager: DumpManager @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener @Mock lateinit var mediaResumeListener: MediaResumeListener @JvmField @Rule val mockito = MockitoJUnit.rule() lateinit var mediaDataManager: MediaDataManager lateinit var mediaNotification: StatusBarNotification @Before fun setup() { mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, useMediaResumption = true, useQsMediaPlayer = true) val sbn = mock(StatusBarNotification::class.java) val notification = mock(Notification::class.java) whenever(notification.hasMediaSession()).thenReturn(true) whenever(notification.notificationStyle).thenReturn(Notification.MediaStyle::class.java) whenever(sbn.notification).thenReturn(notification) whenever(sbn.packageName).thenReturn(PACKAGE_NAME) mediaNotification = sbn } @After fun tearDown() { mediaDataManager.destroy() } @Test fun testHasActiveMedia() { assertThat(mediaDataManager.hasActiveMedia()).isFalse() val data = mock(MediaData::class.java) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) assertThat(mediaDataManager.hasActiveMedia()).isFalse() whenever(data.active).thenReturn(true) assertThat(mediaDataManager.hasActiveMedia()).isTrue() } @Test fun testLoadsMetadataOnBackground() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) verify(backgroundExecutor).execute(anyObject()) } @Test fun testOnMetaDataLoaded_callsListener() { val listener = mock(MediaDataManager.Listener::class.java) mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java)) verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject()) } @Test fun testHasAnyMedia_whenAddingMedia() { assertThat(mediaDataManager.hasAnyMedia()).isFalse() val data = mock(MediaData::class.java) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) assertThat(mediaDataManager.hasAnyMedia()).isTrue() } @Test fun testOnNotificationRemoved_doesntHaveMedia() { val data = mock(MediaData::class.java) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) mediaDataManager.onNotificationRemoved(KEY) assertThat(mediaDataManager.hasAnyMedia()).isFalse() } @Test fun testOnNotificationRemoved_callsListener() { val listener = mock(MediaDataManager.Listener::class.java) mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java)) mediaDataManager.onNotificationRemoved(KEY) verify(listener).onMediaDataRemoved(eq(KEY)) } } No newline at end of file Loading
packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +50 −19 Original line number Original line Diff line number Diff line Loading @@ -38,18 +38,20 @@ import android.service.notification.StatusBarNotification import android.text.TextUtils import android.text.TextUtils import android.util.Log import android.util.Log import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.ColorUtils import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.notification.MediaNotificationProcessor 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.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert import com.android.systemui.util.Assert import com.android.systemui.util.Utils import com.android.systemui.util.Utils import java.io.FileDescriptor import java.io.IOException import java.io.IOException import java.io.PrintWriter import java.util.concurrent.Executor import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Inject import javax.inject.Singleton import javax.inject.Singleton Loading Loading @@ -85,20 +87,35 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean { * A class that facilitates management and loading of Media Data, ready for binding. * A class that facilitates management and loading of Media Data, ready for binding. */ */ @Singleton @Singleton class MediaDataManager @Inject constructor( class MediaDataManager( private val context: Context, private val context: Context, private val mediaControllerFactory: MediaControllerFactory, private val notificationEntryManager: NotificationEntryManager, @Background private val backgroundExecutor: Executor, @Background private val backgroundExecutor: Executor, @Main private val foregroundExecutor: Executor, @Main private val foregroundExecutor: Executor, broadcastDispatcher: BroadcastDispatcher, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, dumpManager: DumpManager, mediaTimeoutListener: MediaTimeoutListener, mediaTimeoutListener: MediaTimeoutListener, mediaResumeListener: MediaResumeListener mediaResumeListener: MediaResumeListener, ) { private val useMediaResumption: Boolean, private val useQsMediaPlayer: Boolean ) : Dumpable { private val listeners: MutableSet<Listener> = mutableSetOf() private val listeners: MutableSet<Listener> = mutableSetOf() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() private val useMediaResumption: Boolean = Utils.useMediaResumption(context) @Inject constructor( context: Context, @Background backgroundExecutor: Executor, @Main foregroundExecutor: Executor, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, broadcastDispatcher: BroadcastDispatcher, mediaTimeoutListener: MediaTimeoutListener, mediaResumeListener: MediaResumeListener ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context)) private val userChangeReceiver = object : BroadcastReceiver() { private val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) { Loading Loading @@ -128,6 +145,7 @@ class MediaDataManager @Inject constructor( } } init { init { dumpManager.registerDumpable(TAG, this) mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> setTimedOut(token, timedOut) } setTimedOut(token, timedOut) } addListener(mediaTimeoutListener) addListener(mediaTimeoutListener) Loading Loading @@ -159,8 +177,13 @@ class MediaDataManager @Inject constructor( context.registerReceiver(appChangeReceiver, uninstallFilter) context.registerReceiver(appChangeReceiver, uninstallFilter) } } fun destroy() { context.unregisterReceiver(appChangeReceiver) broadcastDispatcher.unregisterReceiver(userChangeReceiver) } fun onNotificationAdded(key: String, sbn: StatusBarNotification) { fun onNotificationAdded(key: String, sbn: StatusBarNotification) { if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) { if (useQsMediaPlayer && isMediaNotification(sbn)) { Assert.isMainThread() Assert.isMainThread() val oldKey = findExistingEntry(key, sbn.packageName) val oldKey = findExistingEntry(key, sbn.packageName) if (oldKey == null) { if (oldKey == null) { Loading Loading @@ -253,18 +276,18 @@ class MediaDataManager @Inject constructor( */ */ fun removeListener(listener: Listener) = listeners.remove(listener) fun removeListener(listener: Listener) = listeners.remove(listener) /** * Called whenever the player has been paused or stopped for a while. * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ private fun setTimedOut(token: String, timedOut: Boolean) { private fun setTimedOut(token: String, timedOut: Boolean) { mediaEntries[token]?.let { mediaEntries[token]?.let { if (Utils.useMediaResumption(context)) { if (it.active == !timedOut) { if (it.active == !timedOut) { return return } } it.active = !timedOut it.active = !timedOut onMediaDataLoaded(token, token, it) onMediaDataLoaded(token, token, it) } else if (timedOut) { notificationEntryManager.removeNotification(it.notificationKey, null /* ranking */, UNDEFINED_DISMISS_REASON) } } } } } Loading Loading @@ -570,4 +593,12 @@ class MediaDataManager @Inject constructor( */ */ fun onMediaDataRemoved(key: String) {} fun onMediaDataRemoved(key: String) {} } } override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { pw.apply { println("listeners: $listeners") println("mediaEntries: $mediaEntries") println("useMediaResumption: $useMediaResumption") } } } }
packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt 0 → 100644 +126 −0 Original line number Original line Diff line number Diff line package com.android.systemui.media import android.app.Notification import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import java.util.concurrent.Executor import org.mockito.Mockito.`when` as whenever private const val KEY = "KEY" private const val PACKAGE_NAME = "com.android.systemui" private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> anyObject(): T { return Mockito.anyObject<T>() } @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidTestingRunner::class) class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var mediaControllerFactory: MediaControllerFactory @Mock lateinit var backgroundExecutor: Executor @Mock lateinit var foregroundExecutor: Executor @Mock lateinit var dumpManager: DumpManager @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener @Mock lateinit var mediaResumeListener: MediaResumeListener @JvmField @Rule val mockito = MockitoJUnit.rule() lateinit var mediaDataManager: MediaDataManager lateinit var mediaNotification: StatusBarNotification @Before fun setup() { mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, useMediaResumption = true, useQsMediaPlayer = true) val sbn = mock(StatusBarNotification::class.java) val notification = mock(Notification::class.java) whenever(notification.hasMediaSession()).thenReturn(true) whenever(notification.notificationStyle).thenReturn(Notification.MediaStyle::class.java) whenever(sbn.notification).thenReturn(notification) whenever(sbn.packageName).thenReturn(PACKAGE_NAME) mediaNotification = sbn } @After fun tearDown() { mediaDataManager.destroy() } @Test fun testHasActiveMedia() { assertThat(mediaDataManager.hasActiveMedia()).isFalse() val data = mock(MediaData::class.java) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) assertThat(mediaDataManager.hasActiveMedia()).isFalse() whenever(data.active).thenReturn(true) assertThat(mediaDataManager.hasActiveMedia()).isTrue() } @Test fun testLoadsMetadataOnBackground() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) verify(backgroundExecutor).execute(anyObject()) } @Test fun testOnMetaDataLoaded_callsListener() { val listener = mock(MediaDataManager.Listener::class.java) mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java)) verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject()) } @Test fun testHasAnyMedia_whenAddingMedia() { assertThat(mediaDataManager.hasAnyMedia()).isFalse() val data = mock(MediaData::class.java) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) assertThat(mediaDataManager.hasAnyMedia()).isTrue() } @Test fun testOnNotificationRemoved_doesntHaveMedia() { val data = mock(MediaData::class.java) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) mediaDataManager.onNotificationRemoved(KEY) assertThat(mediaDataManager.hasAnyMedia()).isFalse() } @Test fun testOnNotificationRemoved_callsListener() { val listener = mock(MediaDataManager.Listener::class.java) mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java)) mediaDataManager.onNotificationRemoved(KEY) verify(listener).onMediaDataRemoved(eq(KEY)) } } No newline at end of file