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
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 org.junit.Before
31 import org.junit.Test
32 import org.junit.runner.RunWith
33 import org.mockito.ArgumentCaptor
34 import org.mockito.Captor
35 import org.mockito.Mock
36 import org.mockito.Mockito
37 import org.mockito.Mockito.verify
38 import org.mockito.MockitoAnnotations
39 import org.mockito.Mockito.`when` as whenever
40
41 private const val PACKAGE_NAME = "package"
42 private const val CLASS_NAME = "class"
43 private const val TITLE = "song title"
44 private const val MEDIA_ID = "media ID"
45 private const val ROOT = "media browser root"
46
capturenull47 private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
48 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
49 private fun <T> any(): T = Mockito.any<T>()
50
51 @SmallTest
52 @RunWith(AndroidTestingRunner::class)
53 @TestableLooper.RunWithLooper
54 public class ResumeMediaBrowserTest : SysuiTestCase() {
55
56 private lateinit var resumeBrowser: TestableResumeMediaBrowser
57 private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
58 private val description = MediaDescription.Builder()
59 .setTitle(TITLE)
60 .setMediaId(MEDIA_ID)
61 .build()
62
63 @Mock lateinit var callback: ResumeMediaBrowser.Callback
64 @Mock lateinit var listener: MediaResumeListener
65 @Mock lateinit var service: MediaBrowserService
66 @Mock lateinit var browserFactory: MediaBrowserFactory
67 @Mock lateinit var browser: MediaBrowser
68 @Mock lateinit var token: MediaSession.Token
69 @Mock lateinit var mediaController: MediaController
70 @Mock lateinit var transportControls: MediaController.TransportControls
71
72 @Captor lateinit var connectionCallback: ArgumentCaptor<MediaBrowser.ConnectionCallback>
73 @Captor lateinit var subscriptionCallback: ArgumentCaptor<MediaBrowser.SubscriptionCallback>
74
75 @Before
76 fun setUp() {
77 MockitoAnnotations.initMocks(this)
78
79 whenever(browserFactory.create(any(), capture(connectionCallback), any()))
80 .thenReturn(browser)
81
82 whenever(mediaController.transportControls).thenReturn(transportControls)
83
84 resumeBrowser = TestableResumeMediaBrowser(context, callback, component, browserFactory,
85 mediaController)
86 }
87
88 @Test
89 fun testConnection_connectionFails_callsOnError() {
90 // When testConnection cannot connect to the service
91 setupBrowserFailed()
92 resumeBrowser.testConnection()
93
94 // Then it calls onError and disconnects
95 verify(callback).onError()
96 verify(browser).disconnect()
97 }
98
99 @Test
100 fun testConnection_connects_onConnected() {
101 // When testConnection can connect to the service
102 setupBrowserConnection()
103 resumeBrowser.testConnection()
104
105 // Then it calls onConnected
106 verify(callback).onConnected()
107 }
108
109 @Test
110 fun testConnection_noValidMedia_error() {
111 // When testConnection can connect to the service, and does not find valid media
112 setupBrowserConnectionNoResults()
113 resumeBrowser.testConnection()
114
115 // Then it calls onError and disconnects
116 verify(callback).onError()
117 verify(browser).disconnect()
118 }
119
120 @Test
121 fun testConnection_hasValidMedia_addTrack() {
122 // When testConnection can connect to the service, and finds valid media
123 setupBrowserConnectionValidMedia()
124 resumeBrowser.testConnection()
125
126 // Then it calls addTrack
127 verify(callback).onConnected()
128 verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
129 }
130
131 @Test
132 fun testFindRecentMedia_connectionFails_error() {
133 // When findRecentMedia is called and we cannot connect
134 setupBrowserFailed()
135 resumeBrowser.findRecentMedia()
136
137 // Then it calls onError and disconnects
138 verify(callback).onError()
139 verify(browser).disconnect()
140 }
141
142 @Test
143 fun testFindRecentMedia_noRoot_error() {
144 // When findRecentMedia is called and does not get a valid root
145 setupBrowserConnection()
146 whenever(browser.getRoot()).thenReturn(null)
147 resumeBrowser.findRecentMedia()
148
149 // Then it calls onError and disconnects
150 verify(callback).onError()
151 verify(browser).disconnect()
152 }
153
154 @Test
155 fun testFindRecentMedia_connects_onConnected() {
156 // When findRecentMedia is called and we connect
157 setupBrowserConnection()
158 resumeBrowser.findRecentMedia()
159
160 // Then it calls onConnected
161 verify(callback).onConnected()
162 }
163
164 @Test
165 fun testFindRecentMedia_noChildren_error() {
166 // When findRecentMedia is called and we connect, but do not get any results
167 setupBrowserConnectionNoResults()
168 resumeBrowser.findRecentMedia()
169
170 // Then it calls onError and disconnects
171 verify(callback).onError()
172 verify(browser).disconnect()
173 }
174
175 @Test
176 fun testFindRecentMedia_notPlayable_error() {
177 // When findRecentMedia is called and we connect, but do not get a playable child
178 setupBrowserConnectionNotPlayable()
179 resumeBrowser.findRecentMedia()
180
181 // Then it calls onError and disconnects
182 verify(callback).onError()
183 verify(browser).disconnect()
184 }
185
186 @Test
187 fun testFindRecentMedia_hasValidMedia_addTrack() {
188 // When findRecentMedia is called and we can connect and get playable media
189 setupBrowserConnectionValidMedia()
190 resumeBrowser.findRecentMedia()
191
192 // Then it calls addTrack
193 verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
194 }
195
196 @Test
197 fun testRestart_connectionFails_error() {
198 // When restart is called and we cannot connect
199 setupBrowserFailed()
200 resumeBrowser.restart()
201
202 // Then it calls onError and disconnects
203 verify(callback).onError()
204 verify(browser).disconnect()
205 }
206
207 @Test
208 fun testRestart_connects() {
209 // When restart is called and we connect successfully
210 setupBrowserConnection()
211 resumeBrowser.restart()
212 verify(callback).onConnected()
213
214 // Then it creates a new controller and sends play command
215 verify(transportControls).prepare()
216 verify(transportControls).play()
217 }
218
219 /**
220 * Helper function to mock a failed connection
221 */
222 private fun setupBrowserFailed() {
223 whenever(browser.connect()).thenAnswer {
224 connectionCallback.value.onConnectionFailed()
225 }
226 }
227
228 /**
229 * Helper function to mock a successful connection only
230 */
231 private fun setupBrowserConnection() {
232 whenever(browser.connect()).thenAnswer {
233 connectionCallback.value.onConnected()
234 }
235 whenever(browser.isConnected()).thenReturn(true)
236 whenever(browser.getRoot()).thenReturn(ROOT)
237 whenever(browser.sessionToken).thenReturn(token)
238 }
239
240 /**
241 * Helper function to mock a successful connection, but no media results
242 */
243 private fun setupBrowserConnectionNoResults() {
244 setupBrowserConnection()
245 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
246 subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
247 }
248 }
249
250 /**
251 * Helper function to mock a successful connection, but no playable results
252 */
253 private fun setupBrowserConnectionNotPlayable() {
254 setupBrowserConnection()
255
256 val child = MediaBrowser.MediaItem(description, 0)
257
258 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
259 subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
260 }
261 }
262
263 /**
264 * Helper function to mock a successful connection with playable media
265 */
266 private fun setupBrowserConnectionValidMedia() {
267 setupBrowserConnection()
268
269 val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
270
271 whenever(browser.serviceComponent).thenReturn(component)
272 whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
273 subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
274 }
275 }
276
277 /**
278 * Override so media controller use is testable
279 */
280 private class TestableResumeMediaBrowser(
281 context: Context,
282 callback: Callback,
283 componentName: ComponentName,
284 browserFactory: MediaBrowserFactory,
285 private val fakeController: MediaController
286 ) : ResumeMediaBrowser(context, callback, componentName, browserFactory) {
287
288 override fun createMediaController(token: MediaSession.Token): MediaController {
289 return fakeController
290 }
291 }
292 }