• 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.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