1 package com.android.systemui.media
2
3 import android.app.Notification.MediaStyle
4 import android.app.PendingIntent
5 import android.graphics.Bitmap
6 import android.media.MediaDescription
7 import android.media.MediaMetadata
8 import android.media.session.MediaController
9 import android.media.session.MediaSession
10 import android.service.notification.StatusBarNotification
11 import android.testing.AndroidTestingRunner
12 import android.testing.TestableLooper.RunWithLooper
13 import androidx.test.filters.SmallTest
14 import com.android.systemui.SysuiTestCase
15 import com.android.systemui.broadcast.BroadcastDispatcher
16 import com.android.systemui.dump.DumpManager
17 import com.android.systemui.statusbar.SbnBuilder
18 import com.android.systemui.util.concurrency.FakeExecutor
19 import com.android.systemui.util.mockito.capture
20 import com.android.systemui.util.mockito.eq
21 import com.android.systemui.util.time.FakeSystemClock
22 import com.google.common.truth.Truth.assertThat
23 import org.junit.After
24 import org.junit.Before
25 import org.junit.Rule
26 import org.junit.Test
27 import org.junit.runner.RunWith
28 import org.mockito.ArgumentCaptor
29 import org.mockito.Captor
30 import org.mockito.Mock
31 import org.mockito.Mockito
32 import org.mockito.Mockito.mock
33 import org.mockito.Mockito.never
34 import org.mockito.Mockito.reset
35 import org.mockito.Mockito.verify
36 import org.mockito.junit.MockitoJUnit
37 import org.mockito.Mockito.`when` as whenever
38
39 private const val KEY = "KEY"
40 private const val KEY_2 = "KEY_2"
41 private const val PACKAGE_NAME = "com.android.systemui"
42 private const val APP_NAME = "SystemUI"
43 private const val SESSION_ARTIST = "artist"
44 private const val SESSION_TITLE = "title"
45 private const val USER_ID = 0
46
anyObjectnull47 private fun <T> anyObject(): T {
48 return Mockito.anyObject<T>()
49 }
50
51 @SmallTest
52 @RunWithLooper(setAsMainLooper = true)
53 @RunWith(AndroidTestingRunner::class)
54 class MediaDataManagerTest : SysuiTestCase() {
55
56 @JvmField @Rule val mockito = MockitoJUnit.rule()
57 @Mock lateinit var mediaControllerFactory: MediaControllerFactory
58 @Mock lateinit var controller: MediaController
59 lateinit var session: MediaSession
60 lateinit var metadataBuilder: MediaMetadata.Builder
61 lateinit var backgroundExecutor: FakeExecutor
62 lateinit var foregroundExecutor: FakeExecutor
63 @Mock lateinit var dumpManager: DumpManager
64 @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
65 @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
66 @Mock lateinit var mediaResumeListener: MediaResumeListener
67 @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
68 @Mock lateinit var mediaDeviceManager: MediaDeviceManager
69 @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
70 @Mock lateinit var mediaDataFilter: MediaDataFilter
71 @Mock lateinit var listener: MediaDataManager.Listener
72 @Mock lateinit var pendingIntent: PendingIntent
73 lateinit var mediaDataManager: MediaDataManager
74 lateinit var mediaNotification: StatusBarNotification
75 @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
76
77 @Before
setupnull78 fun setup() {
79 foregroundExecutor = FakeExecutor(FakeSystemClock())
80 backgroundExecutor = FakeExecutor(FakeSystemClock())
81 mediaDataManager = MediaDataManager(
82 context = context,
83 backgroundExecutor = backgroundExecutor,
84 foregroundExecutor = foregroundExecutor,
85 mediaControllerFactory = mediaControllerFactory,
86 broadcastDispatcher = broadcastDispatcher,
87 dumpManager = dumpManager,
88 mediaTimeoutListener = mediaTimeoutListener,
89 mediaResumeListener = mediaResumeListener,
90 mediaSessionBasedFilter = mediaSessionBasedFilter,
91 mediaDeviceManager = mediaDeviceManager,
92 mediaDataCombineLatest = mediaDataCombineLatest,
93 mediaDataFilter = mediaDataFilter,
94 useMediaResumption = true,
95 useQsMediaPlayer = true
96 )
97 session = MediaSession(context, "MediaDataManagerTestSession")
98 mediaNotification = SbnBuilder().run {
99 setPkg(PACKAGE_NAME)
100 modifyNotification(context).also {
101 it.setSmallIcon(android.R.drawable.ic_media_pause)
102 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
103 }
104 build()
105 }
106 metadataBuilder = MediaMetadata.Builder().apply {
107 putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
108 putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
109 }
110 whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
111
112 // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
113 // listeners in the internal processing pipeline. It receives events, but ince it is a
114 // mock, it doesn't pass those events along the chain to the external listeners. So, just
115 // treat mediaSessionBasedFilter as a listener for testing.
116 listener = mediaSessionBasedFilter
117 }
118
119 @After
tearDownnull120 fun tearDown() {
121 session.release()
122 mediaDataManager.destroy()
123 }
124
125 @Test
testSetTimedOut_deactivatesMedianull126 fun testSetTimedOut_deactivatesMedia() {
127 val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null,
128 appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(),
129 actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null,
130 clickIntent = null, device = null, active = true, resumeAction = null)
131 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
132 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data)
133
134 mediaDataManager.setTimedOut(KEY, timedOut = true)
135 assertThat(data.active).isFalse()
136 }
137
138 @Test
testLoadsMetadataOnBackgroundnull139 fun testLoadsMetadataOnBackground() {
140 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
141 assertThat(backgroundExecutor.numPending()).isEqualTo(1)
142 }
143
144 @Test
testOnMetaDataLoaded_callsListenernull145 fun testOnMetaDataLoaded_callsListener() {
146 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
147 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
148 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject())
149 }
150
151 @Test
testOnMetaDataLoaded_conservesActiveFlagnull152 fun testOnMetaDataLoaded_conservesActiveFlag() {
153 whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
154 whenever(controller.metadata).thenReturn(metadataBuilder.build())
155 mediaDataManager.addListener(listener)
156 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
157 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
158 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
159 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
160 assertThat(mediaDataCaptor.value!!.active).isTrue()
161 }
162
163 @Test
testOnNotificationRemoved_callsListenernull164 fun testOnNotificationRemoved_callsListener() {
165 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
166 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
167 mediaDataManager.onNotificationRemoved(KEY)
168 verify(listener).onMediaDataRemoved(eq(KEY))
169 }
170
171 @Test
testOnNotificationRemoved_withResumptionnull172 fun testOnNotificationRemoved_withResumption() {
173 // GIVEN that the manager has a notification with a resume action
174 whenever(controller.metadata).thenReturn(metadataBuilder.build())
175 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
176 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
177 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
178 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
179 val data = mediaDataCaptor.value
180 assertThat(data.resumption).isFalse()
181 mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
182 // WHEN the notification is removed
183 mediaDataManager.onNotificationRemoved(KEY)
184 // THEN the media data indicates that it is for resumption
185 verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
186 assertThat(mediaDataCaptor.value.resumption).isTrue()
187 }
188
189 @Test
testOnNotificationRemoved_twoWithResumptionnull190 fun testOnNotificationRemoved_twoWithResumption() {
191 // GIVEN that the manager has two notifications with resume actions
192 whenever(controller.metadata).thenReturn(metadataBuilder.build())
193 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
194 mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
195 assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
196 assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
197 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
198 val data = mediaDataCaptor.value
199 assertThat(data.resumption).isFalse()
200 val resumableData = data.copy(resumeAction = Runnable {})
201 mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
202 mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
203 reset(listener)
204 // WHEN the first is removed
205 mediaDataManager.onNotificationRemoved(KEY)
206 // THEN the data is for resumption and the key is migrated to the package name
207 verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
208 assertThat(mediaDataCaptor.value.resumption).isTrue()
209 verify(listener, never()).onMediaDataRemoved(eq(KEY))
210 // WHEN the second is removed
211 mediaDataManager.onNotificationRemoved(KEY_2)
212 // THEN the data is for resumption and the second key is removed
213 verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(PACKAGE_NAME),
214 capture(mediaDataCaptor))
215 assertThat(mediaDataCaptor.value.resumption).isTrue()
216 verify(listener).onMediaDataRemoved(eq(KEY_2))
217 }
218
219 @Test
testAddResumptionControlsnull220 fun testAddResumptionControls() {
221 // WHEN resumption controls are added`
222 val desc = MediaDescription.Builder().run {
223 setTitle(SESSION_TITLE)
224 build()
225 }
226 mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
227 APP_NAME, pendingIntent, PACKAGE_NAME)
228 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
229 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
230 // THEN the media data indicates that it is for resumption
231 verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor))
232 val data = mediaDataCaptor.value
233 assertThat(data.resumption).isTrue()
234 assertThat(data.song).isEqualTo(SESSION_TITLE)
235 assertThat(data.app).isEqualTo(APP_NAME)
236 assertThat(data.actions).hasSize(1)
237 }
238
239 @Test
testDismissMedia_listenerCallednull240 fun testDismissMedia_listenerCalled() {
241 mediaDataManager.onNotificationAdded(KEY, mediaNotification)
242 mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
243 mediaDataManager.dismissMediaData(KEY, 0L)
244
245 foregroundExecutor.advanceClockToLast()
246 foregroundExecutor.runAllReady()
247
248 verify(listener).onMediaDataRemoved(eq(KEY))
249 }
250
251 @Test
testBadArtwork_doesNotUsenull252 fun testBadArtwork_doesNotUse() {
253 // WHEN notification has a too-small artwork
254 val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
255 val notif = SbnBuilder().run {
256 setPkg(PACKAGE_NAME)
257 modifyNotification(context).also {
258 it.setSmallIcon(android.R.drawable.ic_media_pause)
259 it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
260 it.setLargeIcon(artwork)
261 }
262 build()
263 }
264 mediaDataManager.onNotificationAdded(KEY, notif)
265
266 // THEN it loads and uses the default background color
267 assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
268 assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
269 verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
270 assertThat(mediaDataCaptor.value!!.backgroundColor).isEqualTo(DEFAULT_COLOR)
271 }
272 }
273