1 /*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.media.controls.resume
18
19 import android.content.ComponentName
20 import android.content.Context
21 import android.media.MediaDescription
22 import android.media.browse.MediaBrowser
23 import android.media.session.MediaController
24 import android.media.session.MediaSession
25 import android.service.media.MediaBrowserService
26 import android.testing.AndroidTestingRunner
27 import android.testing.TestableLooper
28 import androidx.test.filters.SmallTest
29 import com.android.systemui.SysuiTestCase
30 import com.android.systemui.util.mockito.mock
31 import org.junit.Before
32 import org.junit.Test
33 import org.junit.runner.RunWith
34 import org.mockito.ArgumentCaptor
35 import org.mockito.Captor
36 import org.mockito.Mock
37 import org.mockito.Mockito
38 import org.mockito.Mockito.reset
39 import org.mockito.Mockito.verify
40 import org.mockito.Mockito.`when` as whenever
41 import org.mockito.MockitoAnnotations
42
43 private const val PACKAGE_NAME = "package"
44 private const val CLASS_NAME = "class"
45 private const val TITLE = "song title"
46 private const val MEDIA_ID = "media ID"
47 private const val ROOT = "media browser root"
48
capturenull49 private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
50
51 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
52
53 private fun <T> any(): T = Mockito.any<T>()
54
55 @SmallTest
56 @RunWith(AndroidTestingRunner::class)
57 @TestableLooper.RunWithLooper
58 public class ResumeMediaBrowserTest : SysuiTestCase() {
59
60 private lateinit var resumeBrowser: TestableResumeMediaBrowser
61 private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
62 private val description =
63 MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build()
64
65 @Mock lateinit var callback: ResumeMediaBrowser.Callback
66 @Mock lateinit var listener: MediaResumeListener
67 @Mock lateinit var service: MediaBrowserService
68 @Mock lateinit var logger: ResumeMediaBrowserLogger
69 @Mock lateinit var browserFactory: MediaBrowserFactory
70 @Mock lateinit var browser: MediaBrowser
71 @Mock lateinit var token: MediaSession.Token
72 @Mock lateinit var mediaController: MediaController
73 @Mock lateinit var transportControls: MediaController.TransportControls
74
75 @Captor lateinit var connectionCallback: ArgumentCaptor<MediaBrowser.ConnectionCallback>
76 @Captor lateinit var subscriptionCallback: ArgumentCaptor<MediaBrowser.SubscriptionCallback>
77 @Captor lateinit var mediaControllerCallback: ArgumentCaptor<MediaController.Callback>
78
79 @Before
80 fun setUp() {
81 MockitoAnnotations.initMocks(this)
82
83 whenever(browserFactory.create(any(), capture(connectionCallback), any()))
84 .thenReturn(browser)
85
86 whenever(mediaController.transportControls).thenReturn(transportControls)
87 whenever(mediaController.sessionToken).thenReturn(token)
88
89 resumeBrowser =
90 TestableResumeMediaBrowser(
91 context,
92 callback,
93 component,
94 browserFactory,
95 logger,
96 mediaController
97 )
98 }
99
100 @Test
101 fun testConnection_connectionFails_callsOnError() {
102 // When testConnection cannot connect to the service
103 setupBrowserFailed()
104 resumeBrowser.testConnection()
105
106 // Then it calls onError and disconnects
107 verify(callback).onError()
108 verify(browser).disconnect()
109 }
110
111 @Test
112 fun testConnection_connects_onConnected() {
113 // When testConnection can connect to the service
114 setupBrowserConnection()
115 resumeBrowser.testConnection()
116
117 // Then it calls onConnected
118 verify(callback).onConnected()
119 }
120
121 @Test
122 fun testConnection_noValidMedia_error() {
123 // When testConnection can connect to the service, and does not find valid media
124 setupBrowserConnectionNoResults()
125 resumeBrowser.testConnection()
126
127 // Then it calls onError and disconnects
128 verify(callback).onError()
129 verify(browser).disconnect()
130 }
131
132 @Test
133 fun testConnection_hasValidMedia_addTrack() {
134 // When testConnection can connect to the service, and finds valid media
135 setupBrowserConnectionValidMedia()
136 resumeBrowser.testConnection()
137
138 // Then it calls addTrack
139 verify(callback).onConnected()
140 verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
141 }
142
143 @Test
144 fun testConnection_thenSessionDestroyed_disconnects() {
145 // When testConnection is called and we connect successfully
146 setupBrowserConnection()
147 resumeBrowser.testConnection()
148 verify(mediaController).registerCallback(mediaControllerCallback.capture())
149 reset(browser)
150
151 // And a sessionDestroyed event is triggered
152 mediaControllerCallback.value.onSessionDestroyed()
153
154 // Then we disconnect the browser and unregister the callback
155 verify(browser).disconnect()
156 verify(mediaController).unregisterCallback(mediaControllerCallback.value)
157 }
158
159 @Test
160 fun testConnection_calledTwice_oldBrowserDisconnected() {
161 val oldBrowser = mock<MediaBrowser>()
162 whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
163
164 // When testConnection can connect to the service
165 setupBrowserConnection()
166 resumeBrowser.testConnection()
167
168 // And testConnection is called again
169 val newBrowser = mock<MediaBrowser>()
170 whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
171 resumeBrowser.testConnection()
172
173 // Then we disconnect the old browser
174 verify(oldBrowser).disconnect()
175 }
176
177 @Test
178 fun testFindRecentMedia_connectionFails_error() {
179 // When findRecentMedia is called and we cannot connect
180 setupBrowserFailed()
181 resumeBrowser.findRecentMedia()
182
183 // Then it calls onError and disconnects
184 verify(callback).onError()
185 verify(browser).disconnect()
186 }
187
188 @Test
189 fun testFindRecentMedia_noRoot_error() {
190 // When findRecentMedia is called and does not get a valid root
191 setupBrowserConnection()
192 whenever(browser.getRoot()).thenReturn(null)
193 resumeBrowser.findRecentMedia()
194
195 // Then it calls onError and disconnects
196 verify(callback).onError()
197 verify(browser).disconnect()
198 }
199
200 @Test
201 fun testFindRecentMedia_connects_onConnected() {
202 // When findRecentMedia is called and we connect
203 setupBrowserConnection()
204 resumeBrowser.findRecentMedia()
205
206 // Then it calls onConnected
207 verify(callback).onConnected()
208 }
209
210 @Test
211 fun testFindRecentMedia_thenSessionDestroyed_disconnects() {
212 // When findRecentMedia is called and we connect successfully
213 setupBrowserConnection()
214 resumeBrowser.findRecentMedia()
215 verify(mediaController).registerCallback(mediaControllerCallback.capture())
216 reset(browser)
217
218 // And a sessionDestroyed event is triggered
219 mediaControllerCallback.value.onSessionDestroyed()
220
221 // Then we disconnect the browser and unregister the callback
222 verify(browser).disconnect()
223 verify(mediaController).unregisterCallback(mediaControllerCallback.value)
224 }
225
226 @Test
227 fun testFindRecentMedia_calledTwice_oldBrowserDisconnected() {
228 val oldBrowser = mock<MediaBrowser>()
229 whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
230
231 // When findRecentMedia is called and we connect
232 setupBrowserConnection()
233 resumeBrowser.findRecentMedia()
234
235 // And findRecentMedia is called again
236 val newBrowser = mock<MediaBrowser>()
237 whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
238 resumeBrowser.findRecentMedia()
239
240 // Then we disconnect the old browser
241 verify(oldBrowser).disconnect()
242 }
243
244 @Test
245 fun testFindRecentMedia_noChildren_error() {
246 // When findRecentMedia is called and we connect, but do not get any results
247 setupBrowserConnectionNoResults()
248 resumeBrowser.findRecentMedia()
249
250 // Then it calls onError and disconnects
251 verify(callback).onError()
252 verify(browser).disconnect()
253 }
254
255 @Test
256 fun testFindRecentMedia_notPlayable_error() {
257 // When findRecentMedia is called and we connect, but do not get a playable child
258 setupBrowserConnectionNotPlayable()
259 resumeBrowser.findRecentMedia()
260
261 // Then it calls onError and disconnects
262 verify(callback).onError()
263 verify(browser).disconnect()
264 }
265
266 @Test
267 fun testFindRecentMedia_hasValidMedia_addTrack() {
268 // When findRecentMedia is called and we can connect and get playable media
269 setupBrowserConnectionValidMedia()
270 resumeBrowser.findRecentMedia()
271
272 // Then it calls addTrack
273 verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
274 }
275
276 @Test
277 fun testRestart_connectionFails_error() {
278 // When restart is called and we cannot connect
279 setupBrowserFailed()
280 resumeBrowser.restart()
281
282 // Then it calls onError and disconnects
283 verify(callback).onError()
284 verify(browser).disconnect()
285 }
286
287 @Test
288 fun testRestart_connects() {
289 // When restart is called and we connect successfully
290 setupBrowserConnection()
291 resumeBrowser.restart()
292 verify(callback).onConnected()
293
294 // Then it creates a new controller and sends play command
295 verify(transportControls).prepare()
296 verify(transportControls).play()
297 }
298
299 @Test
300 fun testRestart_thenSessionDestroyed_disconnects() {
301 // When restart is called and we connect successfully
302 setupBrowserConnection()
303 resumeBrowser.restart()
304 verify(mediaController).registerCallback(mediaControllerCallback.capture())
305 reset(browser)
306
307 // And a sessionDestroyed event is triggered
308 mediaControllerCallback.value.onSessionDestroyed()
309
310 // Then we disconnect the browser and unregister the callback
311 verify(browser).disconnect()
312 verify(mediaController).unregisterCallback(mediaControllerCallback.value)
313 }
314
315 @Test
316 fun testRestart_calledTwice_oldBrowserDisconnected() {
317 val oldBrowser = mock<MediaBrowser>()
318 whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
319
320 // When restart is called and we connect successfully
321 setupBrowserConnection()
322 resumeBrowser.restart()
323
324 // And restart is called again
325 val newBrowser = mock<MediaBrowser>()
326 whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
327 resumeBrowser.restart()
328
329 // Then we disconnect the old browser
330 verify(oldBrowser).disconnect()
331 }
332
333 /** Helper function to mock a failed connection */
334 private fun setupBrowserFailed() {
335 whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() }
336 }
337
338 /** Helper function to mock a successful connection only */
339 private fun setupBrowserConnection() {
340 whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() }
341 whenever(browser.isConnected()).thenReturn(true)
342 whenever(browser.getRoot()).thenReturn(ROOT)
343 whenever(browser.sessionToken).thenReturn(token)
344 }
345
346 /** Helper function to mock a successful connection, but no media results */
347 private fun setupBrowserConnectionNoResults() {
348 setupBrowserConnection()
349 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
350 subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
351 }
352 }
353
354 /** Helper function to mock a successful connection, but no playable results */
355 private fun setupBrowserConnectionNotPlayable() {
356 setupBrowserConnection()
357
358 val child = MediaBrowser.MediaItem(description, 0)
359
360 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
361 subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
362 }
363 }
364
365 /** Helper function to mock a successful connection with playable media */
366 private fun setupBrowserConnectionValidMedia() {
367 setupBrowserConnection()
368
369 val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
370
371 whenever(browser.serviceComponent).thenReturn(component)
372 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
373 subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
374 }
375 }
376
377 /** Override so media controller use is testable */
378 private class TestableResumeMediaBrowser(
379 context: Context,
380 callback: Callback,
381 componentName: ComponentName,
382 browserFactory: MediaBrowserFactory,
383 logger: ResumeMediaBrowserLogger,
384 private val fakeController: MediaController
385 ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, logger) {
386
387 override fun createMediaController(token: MediaSession.Token): MediaController {
388 return fakeController
389 }
390 }
391 }
392