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