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.domain.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.TestableLooper
27 import androidx.test.ext.junit.runners.AndroidJUnit4
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(AndroidJUnit4::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 context.userId,
98 )
99 }
100
101 @Test
102 fun testConnection_connectionFails_callsOnError() {
103 // When testConnection cannot connect to the service
104 setupBrowserFailed()
105 resumeBrowser.testConnection()
106
107 // Then it calls onError and disconnects
108 verify(callback).onError()
109 verify(browser).disconnect()
110 }
111
112 @Test
113 fun testConnection_connects_onConnected() {
114 // When testConnection can connect to the service
115 setupBrowserConnection()
116 resumeBrowser.testConnection()
117
118 // Then it calls onConnected
119 verify(callback).onConnected()
120 }
121
122 @Test
123 fun testConnection_noValidMedia_error() {
124 // When testConnection can connect to the service, and does not find valid media
125 setupBrowserConnectionNoResults()
126 resumeBrowser.testConnection()
127
128 // Then it calls onError and disconnects
129 verify(callback).onError()
130 verify(browser).disconnect()
131 }
132
133 @Test
134 fun testConnection_hasValidMedia_addTrack() {
135 // When testConnection can connect to the service, and finds valid media
136 setupBrowserConnectionValidMedia()
137 resumeBrowser.testConnection()
138
139 // Then it calls addTrack
140 verify(callback).onConnected()
141 verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
142 }
143
144 @Test
145 fun testConnection_thenSessionDestroyed_disconnects() {
146 // When testConnection is called and we connect successfully
147 setupBrowserConnection()
148 resumeBrowser.testConnection()
149 verify(mediaController).registerCallback(mediaControllerCallback.capture())
150 reset(browser)
151
152 // And a sessionDestroyed event is triggered
153 mediaControllerCallback.value.onSessionDestroyed()
154
155 // Then we disconnect the browser and unregister the callback
156 verify(browser).disconnect()
157 verify(mediaController).unregisterCallback(mediaControllerCallback.value)
158 }
159
160 @Test
161 fun testConnection_calledTwice_oldBrowserDisconnected() {
162 val oldBrowser = mock<MediaBrowser>()
163 whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
164
165 // When testConnection can connect to the service
166 setupBrowserConnection()
167 resumeBrowser.testConnection()
168
169 // And testConnection is called again
170 val newBrowser = mock<MediaBrowser>()
171 whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
172 resumeBrowser.testConnection()
173
174 // Then we disconnect the old browser
175 verify(oldBrowser).disconnect()
176 }
177
178 @Test
179 fun testFindRecentMedia_connectionFails_error() {
180 // When findRecentMedia is called and we cannot connect
181 setupBrowserFailed()
182 resumeBrowser.findRecentMedia()
183
184 // Then it calls onError and disconnects
185 verify(callback).onError()
186 verify(browser).disconnect()
187 }
188
189 @Test
190 fun testFindRecentMedia_noRoot_error() {
191 // When findRecentMedia is called and does not get a valid root
192 setupBrowserConnection()
193 whenever(browser.getRoot()).thenReturn(null)
194 resumeBrowser.findRecentMedia()
195
196 // Then it calls onError and disconnects
197 verify(callback).onError()
198 verify(browser).disconnect()
199 }
200
201 @Test
202 fun testFindRecentMedia_connects_onConnected() {
203 // When findRecentMedia is called and we connect
204 setupBrowserConnection()
205 resumeBrowser.findRecentMedia()
206
207 // Then it calls onConnected
208 verify(callback).onConnected()
209 }
210
211 @Test
212 fun testFindRecentMedia_thenSessionDestroyed_disconnects() {
213 // When findRecentMedia is called and we connect successfully
214 setupBrowserConnection()
215 resumeBrowser.findRecentMedia()
216 verify(mediaController).registerCallback(mediaControllerCallback.capture())
217 reset(browser)
218
219 // And a sessionDestroyed event is triggered
220 mediaControllerCallback.value.onSessionDestroyed()
221
222 // Then we disconnect the browser and unregister the callback
223 verify(browser).disconnect()
224 verify(mediaController).unregisterCallback(mediaControllerCallback.value)
225 }
226
227 @Test
228 fun testFindRecentMedia_calledTwice_oldBrowserDisconnected() {
229 val oldBrowser = mock<MediaBrowser>()
230 whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
231
232 // When findRecentMedia is called and we connect
233 setupBrowserConnection()
234 resumeBrowser.findRecentMedia()
235
236 // And findRecentMedia is called again
237 val newBrowser = mock<MediaBrowser>()
238 whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
239 resumeBrowser.findRecentMedia()
240
241 // Then we disconnect the old browser
242 verify(oldBrowser).disconnect()
243 }
244
245 @Test
246 fun testFindRecentMedia_noChildren_error() {
247 // When findRecentMedia is called and we connect, but do not get any results
248 setupBrowserConnectionNoResults()
249 resumeBrowser.findRecentMedia()
250
251 // Then it calls onError and disconnects
252 verify(callback).onError()
253 verify(browser).disconnect()
254 }
255
256 @Test
257 fun testFindRecentMedia_notPlayable_error() {
258 // When findRecentMedia is called and we connect, but do not get a playable child
259 setupBrowserConnectionNotPlayable()
260 resumeBrowser.findRecentMedia()
261
262 // Then it calls onError and disconnects
263 verify(callback).onError()
264 verify(browser).disconnect()
265 }
266
267 @Test
268 fun testFindRecentMedia_hasValidMedia_addTrack() {
269 // When findRecentMedia is called and we can connect and get playable media
270 setupBrowserConnectionValidMedia()
271 resumeBrowser.findRecentMedia()
272
273 // Then it calls addTrack
274 verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
275 }
276
277 @Test
278 fun testRestart_connectionFails_error() {
279 // When restart is called and we cannot connect
280 setupBrowserFailed()
281 resumeBrowser.restart()
282
283 // Then it calls onError and disconnects
284 verify(callback).onError()
285 verify(browser).disconnect()
286 }
287
288 @Test
289 fun testRestart_connects() {
290 // When restart is called and we connect successfully
291 setupBrowserConnection()
292 resumeBrowser.restart()
293 verify(callback).onConnected()
294
295 // Then it creates a new controller and sends play command
296 verify(transportControls).prepare()
297 verify(transportControls).play()
298 }
299
300 @Test
301 fun testRestart_thenSessionDestroyed_disconnects() {
302 // When restart is called and we connect successfully
303 setupBrowserConnection()
304 resumeBrowser.restart()
305 verify(mediaController).registerCallback(mediaControllerCallback.capture())
306 reset(browser)
307
308 // And a sessionDestroyed event is triggered
309 mediaControllerCallback.value.onSessionDestroyed()
310
311 // Then we disconnect the browser and unregister the callback
312 verify(browser).disconnect()
313 verify(mediaController).unregisterCallback(mediaControllerCallback.value)
314 }
315
316 @Test
317 fun testRestart_calledTwice_oldBrowserDisconnected() {
318 val oldBrowser = mock<MediaBrowser>()
319 whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
320
321 // When restart is called and we connect successfully
322 setupBrowserConnection()
323 resumeBrowser.restart()
324
325 // And restart is called again
326 val newBrowser = mock<MediaBrowser>()
327 whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
328 resumeBrowser.restart()
329
330 // Then we disconnect the old browser
331 verify(oldBrowser).disconnect()
332 }
333
334 /** Helper function to mock a failed connection */
335 private fun setupBrowserFailed() {
336 whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() }
337 }
338
339 /** Helper function to mock a successful connection only */
340 private fun setupBrowserConnection() {
341 whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() }
342 whenever(browser.isConnected()).thenReturn(true)
343 whenever(browser.getRoot()).thenReturn(ROOT)
344 whenever(browser.sessionToken).thenReturn(token)
345 }
346
347 /** Helper function to mock a successful connection, but no media results */
348 private fun setupBrowserConnectionNoResults() {
349 setupBrowserConnection()
350 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
351 subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
352 }
353 }
354
355 /** Helper function to mock a successful connection, but no playable results */
356 private fun setupBrowserConnectionNotPlayable() {
357 setupBrowserConnection()
358
359 val child = MediaBrowser.MediaItem(description, 0)
360
361 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
362 subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
363 }
364 }
365
366 /** Helper function to mock a successful connection with playable media */
367 private fun setupBrowserConnectionValidMedia() {
368 setupBrowserConnection()
369
370 val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
371
372 whenever(browser.serviceComponent).thenReturn(component)
373 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
374 subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
375 }
376 }
377
378 /** Override so media controller use is testable */
379 private class TestableResumeMediaBrowser(
380 context: Context,
381 callback: Callback,
382 componentName: ComponentName,
383 browserFactory: MediaBrowserFactory,
384 logger: ResumeMediaBrowserLogger,
385 private val fakeController: MediaController,
386 userId: Int,
387 ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, logger, userId) {
388
389 override fun createMediaController(token: MediaSession.Token): MediaController {
390 return fakeController
391 }
392 }
393 }
394