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.app.PendingIntent
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.Intent
23 import android.content.SharedPreferences
24 import android.content.pm.PackageManager
25 import android.content.pm.ResolveInfo
26 import android.content.pm.ServiceInfo
27 import android.media.MediaDescription
28 import android.media.session.MediaSession
29 import android.provider.Settings
30 import android.testing.TestableLooper
31 import androidx.test.ext.junit.runners.AndroidJUnit4
32 import androidx.test.filters.SmallTest
33 import com.android.systemui.SysuiTestCase
34 import com.android.systemui.broadcast.BroadcastDispatcher
35 import com.android.systemui.dump.DumpManager
36 import com.android.systemui.media.controls.MediaTestUtils
37 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
38 import com.android.systemui.media.controls.domain.pipeline.RESUME_MEDIA_TIMEOUT
39 import com.android.systemui.media.controls.shared.model.MediaData
40 import com.android.systemui.media.controls.shared.model.MediaDeviceData
41 import com.android.systemui.media.controls.util.MediaFlags
42 import com.android.systemui.settings.UserTracker
43 import com.android.systemui.tuner.TunerService
44 import com.android.systemui.util.concurrency.FakeExecutor
45 import com.android.systemui.util.time.FakeSystemClock
46 import com.google.common.truth.Truth.assertThat
47 import org.junit.After
48 import org.junit.Before
49 import org.junit.Test
50 import org.junit.runner.RunWith
51 import org.mockito.ArgumentCaptor
52 import org.mockito.ArgumentMatchers.anyInt
53 import org.mockito.ArgumentMatchers.isNotNull
54 import org.mockito.Captor
55 import org.mockito.Mock
56 import org.mockito.Mockito
57 import org.mockito.Mockito.mock
58 import org.mockito.Mockito.never
59 import org.mockito.Mockito.times
60 import org.mockito.Mockito.verify
61 import org.mockito.Mockito.`when` as whenever
62 import org.mockito.MockitoAnnotations
63
64 private const val KEY = "TEST_KEY"
65 private const val OLD_KEY = "RESUME_KEY"
66 private const val PACKAGE_NAME = "PKG"
67 private const val CLASS_NAME = "CLASS"
68 private const val TITLE = "TITLE"
69 private const val MEDIA_PREFERENCES = "media_control_prefs"
70 private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
71
capturenull72 private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
73
74 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
75
76 private fun <T> any(): T = Mockito.any<T>()
77
78 @SmallTest
79 @RunWith(AndroidJUnit4::class)
80 @TestableLooper.RunWithLooper
81 class MediaResumeListenerTest : SysuiTestCase() {
82
83 @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
84 @Mock private lateinit var userTracker: UserTracker
85 @Mock private lateinit var mediaDataManager: MediaDataManager
86 @Mock private lateinit var device: MediaDeviceData
87 @Mock private lateinit var token: MediaSession.Token
88 @Mock private lateinit var tunerService: TunerService
89 @Mock private lateinit var resumeBrowserFactory: ResumeMediaBrowserFactory
90 @Mock private lateinit var resumeBrowser: ResumeMediaBrowser
91 @Mock private lateinit var sharedPrefs: SharedPreferences
92 @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor
93 @Mock private lateinit var mockContext: Context
94 @Mock private lateinit var pendingIntent: PendingIntent
95 @Mock private lateinit var dumpManager: DumpManager
96 @Mock private lateinit var mediaFlags: MediaFlags
97
98 @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
99 @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
100 @Captor lateinit var componentCaptor: ArgumentCaptor<String>
101 @Captor lateinit var userIdCaptor: ArgumentCaptor<Int>
102 @Captor lateinit var userCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
103
104 private lateinit var executor: FakeExecutor
105 private lateinit var data: MediaData
106 private lateinit var resumeListener: MediaResumeListener
107 private val clock = FakeSystemClock()
108
109 private var originalQsSetting =
110 Settings.Global.getInt(
111 context.contentResolver,
112 Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
113 1
114 )
115 private var originalResumeSetting =
116 Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
117
118 @Before
119 fun setup() {
120 MockitoAnnotations.initMocks(this)
121
122 Settings.Global.putInt(
123 context.contentResolver,
124 Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
125 1
126 )
127 Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
128
129 whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), capture(userIdCaptor)))
130 .thenReturn(resumeBrowser)
131
132 // resume components are stored in sharedpreferences
133 whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
134 .thenReturn(sharedPrefs)
135 whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
136 whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
137 whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
138 whenever(mockContext.packageManager).thenReturn(context.packageManager)
139 whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
140 whenever(mockContext.userId).thenReturn(context.userId)
141 whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
142
143 executor = FakeExecutor(clock)
144 resumeListener =
145 MediaResumeListener(
146 mockContext,
147 broadcastDispatcher,
148 userTracker,
149 executor,
150 executor,
151 tunerService,
152 resumeBrowserFactory,
153 dumpManager,
154 clock,
155 mediaFlags,
156 )
157 resumeListener.setManager(mediaDataManager)
158 mediaDataManager.addListener(resumeListener)
159
160 data =
161 MediaTestUtils.emptyMediaData.copy(
162 song = TITLE,
163 packageName = PACKAGE_NAME,
164 token = token
165 )
166 }
167
168 @After
169 fun tearDown() {
170 Settings.Global.putInt(
171 context.contentResolver,
172 Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
173 originalQsSetting
174 )
175 Settings.Secure.putInt(
176 context.contentResolver,
177 Settings.Secure.MEDIA_CONTROLS_RESUME,
178 originalResumeSetting
179 )
180 }
181
182 @Test
183 fun testWhenNoResumption_doesNothing() {
184 Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
185
186 // When listener is created, we do NOT register a user change listener
187 val listener =
188 MediaResumeListener(
189 context,
190 broadcastDispatcher,
191 userTracker,
192 executor,
193 executor,
194 tunerService,
195 resumeBrowserFactory,
196 dumpManager,
197 clock,
198 mediaFlags,
199 )
200 listener.setManager(mediaDataManager)
201 verify(broadcastDispatcher, never())
202 .registerReceiver(eq(listener.userUnlockReceiver), any(), any(), any(), anyInt(), any())
203
204 // When data is loaded, we do NOT execute or update anything
205 listener.onMediaDataLoaded(KEY, OLD_KEY, data)
206 assertThat(executor.numPending()).isEqualTo(0)
207 verify(mediaDataManager, never()).setResumeAction(any(), any())
208 }
209
210 @Test
211 fun testOnLoad_checksForResume_noService() {
212 // When media data is loaded that has not been checked yet, and does not have a MBS
213 resumeListener.onMediaDataLoaded(KEY, null, data)
214
215 // Then we report back to the manager
216 verify(mediaDataManager).setResumeAction(KEY, null)
217 }
218
219 @Test
220 fun testOnLoad_checksForResume_badService() {
221 setUpMbsWithValidResolveInfo()
222
223 whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
224
225 // When media data is loaded that has not been checked yet, and does not have a MBS
226 resumeListener.onMediaDataLoaded(KEY, null, data)
227 executor.runAllReady()
228
229 // Then we report back to the manager
230 verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
231 }
232
233 @Test
234 fun testOnLoad_localCast_doesNotCheck() {
235 // When media data is loaded that has not been checked yet, and is a local cast
236 val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
237 resumeListener.onMediaDataLoaded(KEY, null, dataCast)
238
239 // Then we do not take action
240 verify(mediaDataManager, never()).setResumeAction(any(), any())
241 }
242
243 @Test
244 fun testOnload_remoteCast_doesNotCheck() {
245 // When media data is loaded that has not been checked yet, and is a remote cast
246 val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
247 resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
248
249 // Then we do not take action
250 verify(mediaDataManager, never()).setResumeAction(any(), any())
251 }
252
253 @Test
254 fun testOnLoad_localCast_remoteResumeAllowed_doesCheck() {
255 // If local cast media is allowed to resume
256 whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
257
258 // When media data is loaded that has not been checked yet, and is a local cast
259 val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
260 resumeListener.onMediaDataLoaded(KEY, null, dataCast)
261
262 // Then we report back to the manager
263 verify(mediaDataManager).setResumeAction(KEY, null)
264 }
265
266 @Test
267 fun testOnLoad_remoteCast_remoteResumeAllowed_doesCheck() {
268 // If local cast media is allowed to resume
269 whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true)
270
271 // When media data is loaded that has not been checked yet, and is a remote cast
272 val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
273 resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
274
275 // Then we do not take action
276 verify(mediaDataManager, never()).setResumeAction(any(), any())
277 }
278
279 @Test
280 fun testOnLoad_checksForResume_hasService() {
281 setUpMbsWithValidResolveInfo()
282
283 val description = MediaDescription.Builder().setTitle(TITLE).build()
284 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
285 whenever(resumeBrowser.testConnection()).thenAnswer {
286 callbackCaptor.value.addTrack(description, component, resumeBrowser)
287 }
288
289 // When media data is loaded that has not been checked yet, and does have a MBS
290 val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
291 resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
292
293 // Then we test whether the service is valid
294 executor.runAllReady()
295 verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
296 verify(resumeBrowser).testConnection()
297
298 // And since it is, we send info to the manager
299 verify(mediaDataManager).setResumeAction(eq(KEY), isNotNull())
300
301 // But we do not tell it to add new controls
302 verify(mediaDataManager, never())
303 .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
304 }
305
306 @Test
307 fun testOnLoad_doesNotCheckAgain() {
308 // When a media data is loaded that has been checked already
309 var dataCopy = data.copy(hasCheckedForResume = true)
310 resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
311
312 // Then we should not check it again
313 verify(resumeBrowser, never()).testConnection()
314 verify(mediaDataManager, never()).setResumeAction(KEY, null)
315 }
316
317 @Test
318 fun testOnLoadTwice_onlyChecksOnce() {
319 // When data is first loaded,
320 setUpMbsWithValidResolveInfo()
321 resumeListener.onMediaDataLoaded(KEY, null, data)
322
323 // We notify the manager to set a null action
324 verify(mediaDataManager).setResumeAction(KEY, null)
325
326 // If we then get another update from the app before the first check completes
327 assertThat(executor.numPending()).isEqualTo(1)
328 var dataWithCheck = data.copy(hasCheckedForResume = true)
329 resumeListener.onMediaDataLoaded(KEY, null, dataWithCheck)
330
331 // We do not try to start another check
332 assertThat(executor.numPending()).isEqualTo(1)
333 verify(mediaDataManager).setResumeAction(KEY, null)
334 }
335
336 @Test
337 fun testOnUserUnlock_loadsTracks() {
338 // Set up mock service to successfully find valid media
339 setUpMbsWithValidResolveInfo()
340 val description = MediaDescription.Builder().setTitle(TITLE).build()
341 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
342 whenever(resumeBrowser.token).thenReturn(token)
343 whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
344 whenever(resumeBrowser.findRecentMedia()).thenAnswer {
345 callbackCaptor.value.addTrack(description, component, resumeBrowser)
346 }
347
348 // Make sure broadcast receiver is registered
349 resumeListener.setManager(mediaDataManager)
350 verify(broadcastDispatcher)
351 .registerReceiver(
352 eq(resumeListener.userUnlockReceiver),
353 any(),
354 any(),
355 any(),
356 anyInt(),
357 any()
358 )
359
360 // When we get an unlock event
361 val intent = Intent(Intent.ACTION_USER_UNLOCKED)
362 intent.putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
363 resumeListener.userUnlockReceiver.onReceive(context, intent)
364
365 // Then we should attempt to find recent media for each saved component
366 verify(resumeBrowser, times(3)).findRecentMedia()
367
368 // Then since the mock service found media, the manager should be informed
369 verify(mediaDataManager, times(3))
370 .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
371 }
372
373 @Test
374 fun testGetResumeAction_restarts() {
375 setUpMbsWithValidResolveInfo()
376
377 val description = MediaDescription.Builder().setTitle(TITLE).build()
378 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
379 whenever(resumeBrowser.testConnection()).thenAnswer {
380 callbackCaptor.value.addTrack(description, component, resumeBrowser)
381 }
382
383 // When media data is loaded that has not been checked yet, and does have a MBS
384 val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
385 resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
386
387 // Then we test whether the service is valid and set the resume action
388 executor.runAllReady()
389 verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
390 verify(resumeBrowser).testConnection()
391 verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
392
393 // When the resume action is run
394 actionCaptor.value.run()
395
396 // Then we call restart
397 verify(resumeBrowser).restart()
398 }
399
400 @Test
401 fun testOnUserUnlock_missingTime_saves() {
402 val currentTime = clock.currentTimeMillis()
403
404 // When resume components without a last played time are loaded
405 testOnUserUnlock_loadsTracks()
406
407 // Then we save an update with the current time
408 verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
409 componentCaptor.value
410 .split(ResumeMediaBrowser.DELIMITER.toRegex())
411 .dropLastWhile { it.isEmpty() }
412 .forEach {
413 val result = it.split("/")
414 assertThat(result.size).isEqualTo(3)
415 assertThat(result[2].toLong()).isEqualTo(currentTime)
416 }
417 verify(sharedPrefsEditor).apply()
418 }
419
420 @Test
421 fun testLoadComponents_recentlyPlayed_adds() {
422 // Set up browser to return successfully
423 setUpMbsWithValidResolveInfo()
424 val description = MediaDescription.Builder().setTitle(TITLE).build()
425 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
426 whenever(resumeBrowser.token).thenReturn(token)
427 whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
428 whenever(resumeBrowser.findRecentMedia()).thenAnswer {
429 callbackCaptor.value.addTrack(description, component, resumeBrowser)
430 }
431
432 // Set up shared preferences to have a component with a recent lastplayed time
433 val lastPlayed = clock.currentTimeMillis()
434 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
435 whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
436 val resumeListener =
437 MediaResumeListener(
438 mockContext,
439 broadcastDispatcher,
440 userTracker,
441 executor,
442 executor,
443 tunerService,
444 resumeBrowserFactory,
445 dumpManager,
446 clock,
447 mediaFlags,
448 )
449 resumeListener.setManager(mediaDataManager)
450 mediaDataManager.addListener(resumeListener)
451
452 // When we load a component that was played recently
453 val intent = Intent(Intent.ACTION_USER_UNLOCKED)
454 intent.putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
455 resumeListener.userUnlockReceiver.onReceive(mockContext, intent)
456
457 // We add its resume controls
458 verify(resumeBrowser).findRecentMedia()
459 verify(mediaDataManager)
460 .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
461 }
462
463 @Test
464 fun testLoadComponents_old_ignores() {
465 // Set up shared preferences to have a component with an old lastplayed time
466 val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
467 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
468 whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
469 val resumeListener =
470 MediaResumeListener(
471 mockContext,
472 broadcastDispatcher,
473 userTracker,
474 executor,
475 executor,
476 tunerService,
477 resumeBrowserFactory,
478 dumpManager,
479 clock,
480 mediaFlags,
481 )
482 resumeListener.setManager(mediaDataManager)
483 mediaDataManager.addListener(resumeListener)
484
485 // When we load a component that is not recent
486 val intent = Intent(Intent.ACTION_USER_UNLOCKED)
487 intent.putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
488 resumeListener.userUnlockReceiver.onReceive(mockContext, intent)
489
490 // We do not try to add resume controls
491 verify(resumeBrowser, times(0)).findRecentMedia()
492 verify(mediaDataManager, times(0))
493 .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
494 }
495
496 @Test
497 fun testOnLoad_hasService_updatesLastPlayed() {
498 // Set up browser to return successfully
499 val description = MediaDescription.Builder().setTitle(TITLE).build()
500 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
501 whenever(resumeBrowser.token).thenReturn(token)
502 whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
503 whenever(resumeBrowser.findRecentMedia()).thenAnswer {
504 callbackCaptor.value.addTrack(description, component, resumeBrowser)
505 }
506
507 // Set up shared preferences to have a component with a lastplayed time
508 val currentTime = clock.currentTimeMillis()
509 val lastPlayed = currentTime - 1000
510 val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
511 whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
512 val resumeListener =
513 MediaResumeListener(
514 mockContext,
515 broadcastDispatcher,
516 userTracker,
517 executor,
518 executor,
519 tunerService,
520 resumeBrowserFactory,
521 dumpManager,
522 clock,
523 mediaFlags,
524 )
525 resumeListener.setManager(mediaDataManager)
526 mediaDataManager.addListener(resumeListener)
527
528 // When media data is loaded that has not been checked yet, and does have a MBS
529 val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
530 resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
531
532 // Then we store the new lastPlayed time
533 verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
534 componentCaptor.value
535 .split(ResumeMediaBrowser.DELIMITER.toRegex())
536 .dropLastWhile { it.isEmpty() }
537 .forEach {
538 val result = it.split("/")
539 assertThat(result.size).isEqualTo(3)
540 assertThat(result[2].toLong()).isEqualTo(currentTime)
541 }
542 verify(sharedPrefsEditor).apply()
543 }
544
545 @Test
546 fun testOnMediaDataLoaded_newKeyDifferent_oldMediaBrowserDisconnected() {
547 setUpMbsWithValidResolveInfo()
548
549 resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
550 executor.runAllReady()
551
552 resumeListener.onMediaDataLoaded(key = "newKey", oldKey = KEY, data)
553
554 verify(resumeBrowser).disconnect()
555 }
556
557 @Test
558 fun testOnMediaDataLoaded_updatingResumptionListError_mediaBrowserDisconnected() {
559 setUpMbsWithValidResolveInfo()
560
561 // Set up mocks to return with an error
562 whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
563
564 resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
565 executor.runAllReady()
566
567 // Ensure we disconnect the browser
568 verify(resumeBrowser).disconnect()
569 }
570
571 @Test
572 fun testOnMediaDataLoaded_trackAdded_mediaBrowserDisconnected() {
573 setUpMbsWithValidResolveInfo()
574
575 // Set up mocks to return with a track added
576 val description = MediaDescription.Builder().setTitle(TITLE).build()
577 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
578 whenever(resumeBrowser.testConnection()).thenAnswer {
579 callbackCaptor.value.addTrack(description, component, resumeBrowser)
580 }
581
582 resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
583 executor.runAllReady()
584
585 // Ensure we disconnect the browser
586 verify(resumeBrowser).disconnect()
587 }
588
589 @Test
590 fun testResumeAction_oldMediaBrowserDisconnected() {
591 setUpMbsWithValidResolveInfo()
592
593 val description = MediaDescription.Builder().setTitle(TITLE).build()
594 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
595 whenever(resumeBrowser.testConnection()).thenAnswer {
596 callbackCaptor.value.addTrack(description, component, resumeBrowser)
597 }
598
599 // Load media data that will require us to get the resume action
600 val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
601 resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
602 executor.runAllReady()
603 verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
604
605 // Set up our factory to return a new browser so we can verify we disconnected the old one
606 val newResumeBrowser = mock(ResumeMediaBrowser::class.java)
607 whenever(resumeBrowserFactory.create(capture(callbackCaptor), any(), anyInt()))
608 .thenReturn(newResumeBrowser)
609
610 // When the resume action is run
611 actionCaptor.value.run()
612
613 // Then we disconnect the old one
614 verify(resumeBrowser).disconnect()
615 }
616
617 @Test
618 fun testUserUnlocked_userChangeWhileQuerying() {
619 val firstUserId = 1
620 val secondUserId = 2
621 val description = MediaDescription.Builder().setTitle(TITLE).build()
622 val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
623
624 setUpMbsWithValidResolveInfo()
625 whenever(resumeBrowser.token).thenReturn(token)
626 whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
627
628 val unlockIntent =
629 Intent(Intent.ACTION_USER_UNLOCKED).apply {
630 putExtra(Intent.EXTRA_USER_HANDLE, firstUserId)
631 }
632 verify(userTracker).addCallback(capture(userCallbackCaptor), any())
633
634 // When the first user unlocks and we query their recent media
635 userCallbackCaptor.value.onUserChanged(firstUserId, context)
636 resumeListener.userUnlockReceiver.onReceive(context, unlockIntent)
637 whenever(resumeBrowser.userId).thenReturn(userIdCaptor.value)
638 verify(resumeBrowser, times(3)).findRecentMedia()
639
640 // And the user changes before the MBS response is received
641 userCallbackCaptor.value.onUserChanged(secondUserId, context)
642 callbackCaptor.value.addTrack(description, component, resumeBrowser)
643
644 // Then the loaded media is correctly associated with the first user
645 verify(mediaDataManager)
646 .addResumptionControls(
647 eq(firstUserId),
648 eq(description),
649 any(),
650 eq(token),
651 eq(PACKAGE_NAME),
652 eq(pendingIntent),
653 eq(PACKAGE_NAME)
654 )
655 }
656
657 @Test
658 fun testUserUnlocked_noComponent_doesNotQuery() {
659 // Set up a valid MBS, but user does not have the service available
660 setUpMbsWithValidResolveInfo()
661 val pm = mock(PackageManager::class.java)
662 whenever(mockContext.packageManager).thenReturn(pm)
663 whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(null)
664
665 val unlockIntent =
666 Intent(Intent.ACTION_USER_UNLOCKED).apply {
667 putExtra(Intent.EXTRA_USER_HANDLE, context.userId)
668 }
669
670 // When the user is unlocked, but does not have the component installed
671 resumeListener.userUnlockReceiver.onReceive(context, unlockIntent)
672
673 // Then we never attempt to connect to it
674 verify(resumeBrowser, never()).findRecentMedia()
675 }
676
677 /** Sets up mocks to successfully find a MBS that returns valid media. */
678 private fun setUpMbsWithValidResolveInfo() {
679 val pm = mock(PackageManager::class.java)
680 whenever(mockContext.packageManager).thenReturn(pm)
681 val resolveInfo = ResolveInfo()
682 val serviceInfo = ServiceInfo()
683 serviceInfo.packageName = PACKAGE_NAME
684 resolveInfo.serviceInfo = serviceInfo
685 resolveInfo.serviceInfo.name = CLASS_NAME
686 val resumeInfo = listOf(resolveInfo)
687 whenever(pm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(resumeInfo)
688 whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(resolveInfo)
689 whenever(pm.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
690 }
691 }
692