• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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