• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 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.photopicker.features.preview
18 
19 import android.content.ContentProvider
20 import android.content.ContentResolver.EXTRA_SIZE
21 import android.content.Context
22 import android.content.pm.PackageManager
23 import android.content.pm.UserProperties
24 import android.graphics.Point
25 import android.net.Uri
26 import android.os.Bundle
27 import android.os.Parcel
28 import android.os.UserHandle
29 import android.os.UserManager
30 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING
31 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED
32 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE
33 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE
34 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED
35 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED
36 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY
37 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED
38 import android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED
39 import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER
40 import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED
41 import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK
42 import android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER
43 import android.provider.ICloudMediaSurfaceController
44 import android.provider.ICloudMediaSurfaceStateChangedCallback
45 import android.test.mock.MockContentResolver
46 import android.view.Surface
47 import androidx.core.os.bundleOf
48 import androidx.lifecycle.ViewModel
49 import androidx.lifecycle.ViewModelProvider
50 import androidx.lifecycle.ViewModelStore
51 import androidx.test.ext.junit.runners.AndroidJUnit4
52 import androidx.test.filters.SmallTest
53 import androidx.test.platform.app.InstrumentationRegistry
54 import com.android.photopicker.R
55 import com.android.photopicker.core.configuration.provideTestConfigurationFlow
56 import com.android.photopicker.core.selection.SelectionImpl
57 import com.android.photopicker.core.user.UserMonitor
58 import com.android.photopicker.data.model.Media
59 import com.android.photopicker.data.model.MediaSource
60 import com.android.photopicker.test.utils.MockContentProviderWrapper
61 import com.android.photopicker.tests.utils.mockito.capture
62 import com.android.photopicker.tests.utils.mockito.mockSystemService
63 import com.android.photopicker.tests.utils.mockito.nonNullableEq
64 import com.android.photopicker.tests.utils.mockito.whenever
65 import com.google.common.truth.Truth.assertWithMessage
66 import java.time.LocalDateTime
67 import java.time.ZoneOffset
68 import kotlinx.coroutines.ExperimentalCoroutinesApi
69 import kotlinx.coroutines.flow.first
70 import kotlinx.coroutines.flow.toList
71 import kotlinx.coroutines.launch
72 import kotlinx.coroutines.test.StandardTestDispatcher
73 import kotlinx.coroutines.test.advanceTimeBy
74 import kotlinx.coroutines.test.runTest
75 import org.junit.Before
76 import org.junit.Test
77 import org.junit.runner.RunWith
78 import org.mockito.ArgumentCaptor
79 import org.mockito.Captor
80 import org.mockito.Mock
81 import org.mockito.Mockito.any
82 import org.mockito.Mockito.anyInt
83 import org.mockito.Mockito.anyString
84 import org.mockito.Mockito.isNull
85 import org.mockito.Mockito.times
86 import org.mockito.Mockito.verify
87 import org.mockito.MockitoAnnotations
88 
89 @SmallTest
90 @RunWith(AndroidJUnit4::class)
91 @OptIn(ExperimentalCoroutinesApi::class)
92 class PreviewViewModelTest {
93 
94     @Mock lateinit var mockContext: Context
95     @Mock lateinit var mockUserManager: UserManager
96     @Mock lateinit var mockPackageManager: PackageManager
97     @Mock lateinit var mockContentProvider: ContentProvider
98     @Mock lateinit var mockController: ICloudMediaSurfaceController.Stub
99     @Captor lateinit var controllerBundle: ArgumentCaptor<Bundle>
100 
101     private lateinit var mockContentResolver: MockContentResolver
102 
103     private val USER_HANDLE_PRIMARY: UserHandle
104     private val USER_ID_PRIMARY: Int = 0
105 
106     init {
107         val parcel1 = Parcel.obtain()
108         parcel1.writeInt(USER_ID_PRIMARY)
109         parcel1.setDataPosition(0)
110         USER_HANDLE_PRIMARY = UserHandle(parcel1)
111     }
112 
113     val TEST_MEDIA_IMAGE =
114         Media.Image(
115             mediaId = "id",
116             pickerId = 1000L,
117             authority = "a",
118             mediaSource = MediaSource.LOCAL,
119             mediaUri =
120                 Uri.EMPTY.buildUpon()
<lambda>null121                     .apply {
122                         scheme("content")
123                         authority("media")
124                         path("picker")
125                         path("a")
126                         path("id")
127                     }
128                     .build(),
129             glideLoadableUri =
130                 Uri.EMPTY.buildUpon()
<lambda>null131                     .apply {
132                         scheme("content")
133                         authority(MockContentProviderWrapper.AUTHORITY)
134                         path("id")
135                     }
136                     .build(),
137             dateTakenMillisLong = 123456789L,
138             sizeInBytes = 1000L,
139             mimeType = "image/png",
140             standardMimeTypeExtension = 1,
141         )
142 
143     val TEST_MEDIA_VIDEO =
144         Media.Video(
145             mediaId = "video_id",
146             pickerId = 987654321L,
147             authority = MockContentProviderWrapper.AUTHORITY,
148             mediaSource = MediaSource.LOCAL,
149             mediaUri =
150                 Uri.EMPTY.buildUpon()
<lambda>null151                     .apply {
152                         scheme("content")
153                         authority("a")
154                         path("video_id")
155                     }
156                     .build(),
157             glideLoadableUri =
158                 Uri.EMPTY.buildUpon()
<lambda>null159                     .apply {
160                         scheme("content")
161                         authority("a")
162                         path("video_id")
163                     }
164                     .build(),
165             dateTakenMillisLong = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000,
166             sizeInBytes = 1000L,
167             mimeType = "video/mp4",
168             standardMimeTypeExtension = 1,
169             duration = 10000,
170         )
171 
172     @Before
setupnull173     fun setup() {
174         MockitoAnnotations.initMocks(this)
175         mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
176         whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
177             UserProperties.Builder().build()
178         }
179 
180         // Stub for MockContentResolver constructor
181         whenever(mockContext.getApplicationInfo()) {
182             InstrumentationRegistry.getInstrumentation().getContext().getApplicationInfo()
183         }
184         mockContentResolver = MockContentResolver(mockContext)
185         val provider = MockContentProviderWrapper(mockContentProvider)
186         mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider)
187 
188         // Stubs for UserMonitor
189         whenever(mockContext.packageManager) { mockPackageManager }
190         whenever(mockContext.contentResolver) { mockContentResolver }
191         whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext }
192         whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
193             mockContext
194         }
195         whenever(mockUserManager.getUserBadge()) {
196             InstrumentationRegistry.getInstrumentation()
197                 .getContext()
198                 .getResources()
199                 .getDrawable(R.drawable.android, /* theme= */ null)
200         }
201         whenever(mockUserManager.getProfileLabel()) { "label" }
202 
203         // Stubs for creating the RemoteSurfaceController
204         whenever(
205             mockContentProvider.call(
206                 /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY),
207                 /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER),
208                 /*arg=*/ isNull(),
209                 /*extras=*/ capture(controllerBundle),
210             )
211         ) {
212             bundleOf(EXTRA_SURFACE_CONTROLLER to mockController)
213         }
214     }
215 
216     /** Ensures the view model can toggle items in the session selection. */
217     @Test
testToggleInSelectionUpdatesSelectionnull218     fun testToggleInSelectionUpdatesSelection() {
219 
220         runTest {
221             val selection =
222                 SelectionImpl<Media>(
223                     scope = this.backgroundScope,
224                     configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
225                 )
226 
227             val viewModel =
228                 PreviewViewModel(
229                     this.backgroundScope,
230                     selection,
231                     UserMonitor(
232                         mockContext,
233                         provideTestConfigurationFlow(scope = this.backgroundScope),
234                         this.backgroundScope,
235                         StandardTestDispatcher(this.testScheduler),
236                         USER_HANDLE_PRIMARY
237                     ),
238                 )
239 
240             assertWithMessage("Unexpected selection start size")
241                 .that(selection.snapshot().size)
242                 .isEqualTo(0)
243 
244             // Toggle the item into the selection
245             viewModel.toggleInSelection(TEST_MEDIA_IMAGE, {})
246 
247             // Wait for selection update.
248             advanceTimeBy(100)
249 
250             assertWithMessage("Selection did not contain expected item")
251                 .that(selection.snapshot())
252                 .contains(TEST_MEDIA_IMAGE)
253 
254             // Toggle the item out of the selection
255             viewModel.toggleInSelection(TEST_MEDIA_IMAGE, {})
256 
257             advanceTimeBy(100)
258 
259             assertWithMessage("Selection contains unexpected item")
260                 .that(selection.snapshot())
261                 .doesNotContain(TEST_MEDIA_IMAGE)
262         }
263     }
264 
265     /** Ensures the selection is not snapshotted until requested. */
266     @Test
testSnapshotSelectionnull267     fun testSnapshotSelection() {
268 
269         runTest {
270             val selection =
271                 SelectionImpl<Media>(
272                     scope = this.backgroundScope,
273                     configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
274                     initialSelection = setOf(TEST_MEDIA_IMAGE),
275                 )
276 
277             val viewModel =
278                 PreviewViewModel(
279                     this.backgroundScope,
280                     selection,
281                     UserMonitor(
282                         mockContext,
283                         provideTestConfigurationFlow(scope = this.backgroundScope),
284                         this.backgroundScope,
285                         StandardTestDispatcher(this.testScheduler),
286                         USER_HANDLE_PRIMARY
287                     ),
288                 )
289 
290             var snapshot = viewModel.selectionSnapshot.first()
291 
292             assertWithMessage("Selection snapshot did not match expected")
293                 .that(snapshot)
294                 .isEqualTo(emptySet<Media>())
295 
296             viewModel.takeNewSelectionSnapshot()
297 
298             // Wait for snapshot
299             advanceTimeBy(100)
300 
301             snapshot = viewModel.selectionSnapshot.first()
302 
303             assertWithMessage("Selection snapshot did not match expected")
304                 .that(snapshot)
305                 .isEqualTo(setOf(TEST_MEDIA_IMAGE))
306         }
307     }
308 
309     /** Ensures the creation parameters of remote surface controllers. */
310     @Test
testRemotePreviewControllerCreationnull311     fun testRemotePreviewControllerCreation() {
312 
313         runTest {
314             val selection =
315                 SelectionImpl<Media>(
316                     scope = this.backgroundScope,
317                     configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
318                     initialSelection = setOf(TEST_MEDIA_IMAGE),
319                 )
320             val viewModel =
321                 PreviewViewModel(
322                     this.backgroundScope,
323                     selection,
324                     UserMonitor(
325                         mockContext,
326                         provideTestConfigurationFlow(scope = this.backgroundScope),
327                         this.backgroundScope,
328                         StandardTestDispatcher(this.testScheduler),
329                         USER_HANDLE_PRIMARY
330                     ),
331                 )
332 
333             val controller =
334                 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
335 
336             assertWithMessage("Returned controller was not expected to be null")
337                 .that(controller)
338                 .isNotNull()
339 
340             verify(mockContentProvider)
341                 .call(
342                     /*authority=*/ anyString(),
343                     /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER),
344                     /*arg=*/ isNull(),
345                     /*extras=*/ any(Bundle::class.java),
346                 )
347 
348             val bundle = controllerBundle.getValue()
349             assertWithMessage("SurfaceStateChangedCallback was not provided")
350                 .that(bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK))
351                 .isNotNull()
352             assertWithMessage("Surface controller was not looped by default")
353                 // Default value from bundle is false so this fails if it wasn't set
354                 .that(bundle.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false))
355                 .isTrue()
356             assertWithMessage("Surface controller was not muted by default")
357                 // Default value from bundle is false so this fails if it wasn't set
358                 .that(bundle.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, false))
359                 .isTrue()
360         }
361     }
362 
363     /** Ensures that remote preview controllers are cached for authorities. */
364     @Test
testRemotePreviewControllersAreCachednull365     fun testRemotePreviewControllersAreCached() {
366 
367         runTest {
368             val selection =
369                 SelectionImpl<Media>(
370                     scope = this.backgroundScope,
371                     configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
372                     initialSelection = setOf(TEST_MEDIA_IMAGE),
373                 )
374             val viewModel =
375                 PreviewViewModel(
376                     this.backgroundScope,
377                     selection,
378                     UserMonitor(
379                         mockContext,
380                         provideTestConfigurationFlow(scope = this.backgroundScope),
381                         this.backgroundScope,
382                         StandardTestDispatcher(this.testScheduler),
383                         USER_HANDLE_PRIMARY
384                     ),
385                 )
386 
387             val controller =
388                 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
389             val controllerTwo =
390                 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
391 
392             assertWithMessage("Returned controller was not expected to be null")
393                 .that(controller)
394                 .isNotNull()
395 
396             assertWithMessage("Returned controller was not expected to be null")
397                 .that(controllerTwo)
398                 .isNotNull()
399 
400             assertWithMessage("Expected both controller instances to be the same")
401                 .that(controller)
402                 .isEqualTo(controllerTwo)
403 
404             verify(mockContentProvider, times(1))
405                 .call(
406                     /*authority=*/ anyString(),
407                     /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER),
408                     /*arg=*/ isNull(),
409                     /*extras=*/ any(Bundle::class.java),
410                 )
411         }
412     }
413 
414     /** Ensures that remote preview controllers are destroyed when the view model is cleared. */
415     @Test
testRemotePreviewControllersAreDestroyednull416     fun testRemotePreviewControllersAreDestroyed() {
417 
418         runTest {
419             // Setup a proxy to call the mocked controller, since IBinder uses onTransact under the
420             // hood and that is more complicated to verify.
421             val controllerProxy =
422                 object : ICloudMediaSurfaceController.Stub() {
423 
424                     override fun onSurfaceCreated(
425                         surfaceId: Int,
426                         surface: Surface,
427                         mediaId: String
428                     ) {}
429 
430                     override fun onSurfaceChanged(
431                         surfaceId: Int,
432                         format: Int,
433                         width: Int,
434                         height: Int
435                     ) {}
436 
437                     override fun onSurfaceDestroyed(surfaceId: Int) {}
438                     override fun onMediaPlay(surfaceId: Int) {}
439                     override fun onMediaPause(surfaceId: Int) {}
440                     override fun onMediaSeekTo(surfaceId: Int, timestampMillis: Long) {}
441                     override fun onConfigChange(bundle: Bundle) {}
442                     override fun onDestroy() {
443                         mockController.onDestroy()
444                     }
445                     override fun onPlayerCreate() {}
446                     override fun onPlayerRelease() {}
447                 }
448 
449             whenever(
450                 mockContentProvider.call(
451                     /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY),
452                     /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER),
453                     /*arg=*/ isNull(),
454                     /*extras=*/ capture(controllerBundle),
455                 )
456             ) {
457                 bundleOf(EXTRA_SURFACE_CONTROLLER to controllerProxy)
458             }
459             val selection =
460                 SelectionImpl<Media>(
461                     scope = this.backgroundScope,
462                     configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
463                     initialSelection = setOf(TEST_MEDIA_IMAGE),
464                 )
465             val viewModel =
466                 PreviewViewModel(
467                     this.backgroundScope,
468                     selection,
469                     UserMonitor(
470                         mockContext,
471                         provideTestConfigurationFlow(scope = this.backgroundScope),
472                         this.backgroundScope,
473                         StandardTestDispatcher(this.testScheduler),
474                         USER_HANDLE_PRIMARY
475                     ),
476                 )
477 
478             viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
479 
480             viewModel.callOnCleared()
481             verify(mockController).onDestroy()
482         }
483     }
484 
485     /** Ensures that surface playback updates are emitted. */
486     @Test
testRemotePreviewSurfaceStateChangedCallbackEmitsUpdatesnull487     fun testRemotePreviewSurfaceStateChangedCallbackEmitsUpdates() {
488 
489         runTest {
490             val selection =
491                 SelectionImpl<Media>(
492                     scope = this.backgroundScope,
493                     configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
494                     initialSelection = setOf(TEST_MEDIA_IMAGE),
495                 )
496             val viewModel =
497                 PreviewViewModel(
498                     this.backgroundScope,
499                     selection,
500                     UserMonitor(
501                         mockContext,
502                         provideTestConfigurationFlow(scope = this.backgroundScope),
503                         this.backgroundScope,
504                         StandardTestDispatcher(this.testScheduler),
505                         USER_HANDLE_PRIMARY
506                     ),
507                 )
508 
509             viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
510 
511             val bundle = controllerBundle.getValue()
512             val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK)
513             val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder)
514 
515             val emissions = mutableListOf<PlaybackInfo>()
516             backgroundScope.launch {
517                 viewModel
518                     .getPlaybackInfoForPlayer(
519                         surfaceId = 1,
520                         video = TEST_MEDIA_VIDEO,
521                     )
522                     .toList(emissions)
523             }
524 
525             callback.setPlaybackState(
526                 1,
527                 PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
528                 bundleOf(EXTRA_SIZE to Point(100, 200))
529             )
530             advanceTimeBy(100)
531 
532             val mediaSizeChangedInfo = emissions.removeFirst()
533             assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid")
534                 .that(mediaSizeChangedInfo.state)
535                 .isEqualTo(PlaybackState.MEDIA_SIZE_CHANGED)
536             assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid")
537                 .that(mediaSizeChangedInfo.surfaceId)
538                 .isEqualTo(1)
539             assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid")
540                 .that(mediaSizeChangedInfo.authority)
541                 .isEqualTo(MockContentProviderWrapper.AUTHORITY)
542             assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid")
543                 .that(
544                     mediaSizeChangedInfo.playbackStateInfo?.getParcelable(
545                         EXTRA_SIZE,
546                         Point::class.java
547                     )
548                 )
549                 .isEqualTo(Point(100, 200))
550 
551             callback.setPlaybackState(1, PLAYBACK_STATE_BUFFERING, null)
552             advanceTimeBy(100)
553             assertWithMessage("BUFFERING emitted state was invalid")
554                 .that(emissions.removeFirst())
555                 .isEqualTo(
556                     PlaybackInfo(
557                         state = PlaybackState.BUFFERING,
558                         surfaceId = 1,
559                         authority = MockContentProviderWrapper.AUTHORITY
560                     )
561                 )
562 
563             callback.setPlaybackState(1, PLAYBACK_STATE_READY, null)
564             advanceTimeBy(100)
565             assertWithMessage("READY emitted state was invalid")
566                 .that(emissions.removeFirst())
567                 .isEqualTo(
568                     PlaybackInfo(
569                         state = PlaybackState.READY,
570                         surfaceId = 1,
571                         authority = MockContentProviderWrapper.AUTHORITY
572                     )
573                 )
574 
575             callback.setPlaybackState(1, PLAYBACK_STATE_STARTED, null)
576             advanceTimeBy(100)
577             assertWithMessage("STARTED emitted state was invalid")
578                 .that(emissions.removeFirst())
579                 .isEqualTo(
580                     PlaybackInfo(
581                         state = PlaybackState.STARTED,
582                         surfaceId = 1,
583                         authority = MockContentProviderWrapper.AUTHORITY
584                     )
585                 )
586 
587             callback.setPlaybackState(1, PLAYBACK_STATE_PAUSED, null)
588             advanceTimeBy(100)
589             assertWithMessage("PAUSED emitted state was invalid")
590                 .that(emissions.removeFirst())
591                 .isEqualTo(
592                     PlaybackInfo(
593                         state = PlaybackState.PAUSED,
594                         surfaceId = 1,
595                         authority = MockContentProviderWrapper.AUTHORITY
596                     )
597                 )
598 
599             callback.setPlaybackState(1, PLAYBACK_STATE_COMPLETED, null)
600             advanceTimeBy(100)
601             assertWithMessage("COMPLETED emitted state was invalid")
602                 .that(emissions.removeFirst())
603                 .isEqualTo(
604                     PlaybackInfo(
605                         state = PlaybackState.COMPLETED,
606                         surfaceId = 1,
607                         authority = MockContentProviderWrapper.AUTHORITY
608                     )
609                 )
610 
611             callback.setPlaybackState(1, PLAYBACK_STATE_ERROR_PERMANENT_FAILURE, null)
612             advanceTimeBy(100)
613             assertWithMessage("ERROR_PERMANENT_FAILURE emitted state was invalid")
614                 .that(emissions.removeFirst())
615                 .isEqualTo(
616                     PlaybackInfo(
617                         state = PlaybackState.ERROR_PERMANENT_FAILURE,
618                         surfaceId = 1,
619                         authority = MockContentProviderWrapper.AUTHORITY
620                     )
621                 )
622 
623             callback.setPlaybackState(1, PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE, null)
624             advanceTimeBy(100)
625             assertWithMessage("ERROR_RETRIABLE_FAILURE emitted state was invalid")
626                 .that(emissions.removeFirst())
627                 .isEqualTo(
628                     PlaybackInfo(
629                         state = PlaybackState.ERROR_RETRIABLE_FAILURE,
630                         surfaceId = 1,
631                         authority = MockContentProviderWrapper.AUTHORITY
632                     )
633                 )
634         }
635     }
636 
637     /**
638      * Extension function that will create new [ViewModelStore], add view model into it using
639      * [ViewModelProvider] and then call [ViewModelStore.clear], that will cause
640      * [ViewModel.onCleared] to be called
641      */
callOnClearednull642     private fun ViewModel.callOnCleared() {
643         val viewModelStore = ViewModelStore()
644         val viewModelProvider =
645             ViewModelProvider(
646                 viewModelStore,
647                 object : ViewModelProvider.Factory {
648 
649                     @Suppress("UNCHECKED_CAST")
650                     override fun <T : ViewModel> create(modelClass: Class<T>): T =
651                         this@callOnCleared as T
652                 }
653             )
654         viewModelProvider.get(this@callOnCleared::class.java)
655         viewModelStore.clear() // To call clear() in ViewModel
656     }
657 }
658