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