1 /*
2  * Copyright (C) 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.data
18 
19 import android.content.ContentResolver
20 import android.database.ContentObserver
21 import android.net.Uri
22 import androidx.paging.PagingSource
23 import androidx.test.ext.junit.runners.AndroidJUnit4
24 import androidx.test.filters.SmallTest
25 import com.android.photopicker.core.configuration.provideTestConfigurationFlow
26 import com.android.photopicker.core.configuration.testPhotopickerConfiguration
27 import com.android.photopicker.core.events.RegisteredEventClass
28 import com.android.photopicker.core.features.FeatureManager
29 import com.android.photopicker.core.user.UserProfile
30 import com.android.photopicker.core.user.UserStatus
31 import com.android.photopicker.data.model.Group
32 import com.android.photopicker.data.model.Media
33 import com.android.photopicker.data.model.MediaPageKey
34 import com.android.photopicker.data.model.MediaSource
35 import com.android.photopicker.data.model.Provider
36 import com.android.photopicker.features.cloudmedia.CloudMediaFeature
37 import com.android.photopicker.tests.utils.mockito.nonNullableAny
38 import com.android.photopicker.tests.utils.mockito.nonNullableEq
39 import com.google.common.truth.Truth.assertThat
40 import kotlinx.coroutines.ExperimentalCoroutinesApi
41 import kotlinx.coroutines.flow.MutableStateFlow
42 import kotlinx.coroutines.flow.StateFlow
43 import kotlinx.coroutines.flow.toList
44 import kotlinx.coroutines.flow.update
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.test.StandardTestDispatcher
47 import kotlinx.coroutines.test.TestScope
48 import kotlinx.coroutines.test.advanceTimeBy
49 import kotlinx.coroutines.test.runTest
50 import org.junit.Before
51 import org.junit.Test
52 import org.junit.runner.RunWith
53 import org.mockito.ArgumentMatchers
54 import org.mockito.Mockito.mock
55 import org.mockito.Mockito.times
56 import org.mockito.Mockito.verify
57 
58 @SmallTest
59 @RunWith(AndroidJUnit4::class)
60 @OptIn(ExperimentalCoroutinesApi::class)
61 class DataServiceImplTest {
62     companion object {
63         private val albumMediaUpdateUri =
64             Uri.parse("content://media/picker_internal/v2/album/update")
65         private val mediaUpdateUri = Uri.parse("content://media/picker_internal/v2/media/update")
66         private val availableProvidersUpdateUri =
67             Uri.parse("content://media/picker_internal/v2/available_providers/update")
68         private val userProfilePrimary: UserProfile =
69             UserProfile(identifier = 0, profileType = UserProfile.ProfileType.PRIMARY)
70         private val userProfileManaged: UserProfile =
71             UserProfile(identifier = 10, profileType = UserProfile.ProfileType.MANAGED)
72     }
73 
74     private lateinit var testFeatureManager: FeatureManager
75     private lateinit var testContentProvider: TestMediaProvider
76     private lateinit var testContentResolver: ContentResolver
77     private lateinit var notificationService: TestNotificationServiceImpl
78     private lateinit var mediaProviderClient: MediaProviderClient
79     private lateinit var userStatus: UserStatus
80 
81     @Before
setupnull82     fun setup() {
83         val scope = TestScope()
84         testContentProvider = TestMediaProvider()
85         testContentResolver = ContentResolver.wrap(testContentProvider)
86         notificationService = TestNotificationServiceImpl()
87         mediaProviderClient = MediaProviderClient()
88         userStatus =
89             UserStatus(
90                 activeUserProfile = userProfilePrimary,
91                 allProfiles = listOf(userProfilePrimary),
92                 activeContentResolver = testContentResolver
93             )
94         testFeatureManager =
95             FeatureManager(
96                 provideTestConfigurationFlow(scope = scope.backgroundScope),
97                 scope,
98                 setOf(CloudMediaFeature.Registration),
99                 setOf<RegisteredEventClass>(),
100                 setOf<RegisteredEventClass>(),
101             )
102     }
103 
104     @Test
<lambda>null105     fun testAvailableContentProviderFlow() = runTest {
106         val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
107 
108         val dataService: DataService =
109             DataServiceImpl(
110                 userStatus = userStatusFlow,
111                 scope = this.backgroundScope,
112                 notificationService = notificationService,
113                 mediaProviderClient = mediaProviderClient,
114                 dispatcher = StandardTestDispatcher(this.testScheduler),
115                 config = provideTestConfigurationFlow(this.backgroundScope),
116                 featureManager = testFeatureManager,
117             )
118 
119         val emissions = mutableListOf<List<Provider>>()
120         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
121         advanceTimeBy(100)
122 
123         // The first emission will be an empty string. The next emission will happen once Media
124         // Provider responds with the result of available providers.
125         assertThat(emissions.count()).isEqualTo(2)
126         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
127 
128         assertThat(emissions.get(1).count()).isEqualTo(1)
129         assertThat(emissions.get(1).get(0).authority)
130             .isEqualTo(testContentProvider.providers[0].authority)
131     }
132 
133     @Test
<lambda>null134     fun testInitialAllowedProvider() = runTest {
135         val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
136 
137         val dataService: DataService =
138             DataServiceImpl(
139                 userStatus = userStatusFlow,
140                 scope = this.backgroundScope,
141                 notificationService = notificationService,
142                 mediaProviderClient = mediaProviderClient,
143                 dispatcher = StandardTestDispatcher(this.testScheduler),
144                 config = provideTestConfigurationFlow(this.backgroundScope),
145                 featureManager = testFeatureManager,
146             )
147 
148         val emissions = mutableListOf<List<Provider>>()
149         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
150         advanceTimeBy(100)
151 
152         // The first emission will be an empty string. The next emission will happen once Media
153         // Provider responds with the result of available providers.
154         assertThat(emissions.count()).isEqualTo(2)
155         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
156 
157         assertThat(emissions.get(1).count()).isEqualTo(1)
158         assertThat(emissions.get(1).get(0).authority)
159             .isEqualTo(testContentProvider.providers[0].authority)
160     }
161 
162     @Test
<lambda>null163     fun testUpdateAvailableProviders() = runTest {
164         val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
165 
166         val dataService: DataService =
167             DataServiceImpl(
168                 userStatus = userStatusFlow,
169                 scope = this.backgroundScope,
170                 notificationService = notificationService,
171                 mediaProviderClient = mediaProviderClient,
172                 dispatcher = StandardTestDispatcher(this.testScheduler),
173                 config = provideTestConfigurationFlow(this.backgroundScope),
174                 featureManager = testFeatureManager,
175             )
176 
177         val emissions = mutableListOf<List<Provider>>()
178         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
179         advanceTimeBy(100)
180 
181         assertThat(emissions.count()).isEqualTo(2)
182         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
183 
184         testContentProvider.providers =
185             mutableListOf(
186                 Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
187                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
188             )
189 
190         notificationService.dispatchChangeToObservers(availableProvidersUpdateUri)
191 
192         advanceTimeBy(100)
193 
194         assertThat(emissions.count()).isEqualTo(3)
195 
196         // The first emission will be an empty list.
197         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
198 
199         // The next emission will happen once Media Provider responds with the result of
200         // available providers at the time of init.
201         assertThat(emissions.get(1).count()).isEqualTo(1)
202         assertThat(emissions.get(1).get(0).authority).isEqualTo("test_authority")
203 
204         // The next emission happens when a change notification is dispatched.
205         assertThat(emissions.get(2).count()).isEqualTo(2)
206         assertThat(emissions.get(2).get(0).authority)
207             .isEqualTo(testContentProvider.providers[0].authority)
208         assertThat(emissions.get(2).get(1).authority)
209             .isEqualTo(testContentProvider.providers[1].authority)
210     }
211 
212     @Test
<lambda>null213     fun testAvailableProvidersCloudMediaFeatureDisabled() = runTest {
214         val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
215         val scope = TestScope()
216         val dataService: DataService =
217             DataServiceImpl(
218                 userStatus = userStatusFlow,
219                 scope = this.backgroundScope,
220                 notificationService = notificationService,
221                 mediaProviderClient = mediaProviderClient,
222                 dispatcher = StandardTestDispatcher(this.testScheduler),
223                 config = MutableStateFlow(testPhotopickerConfiguration),
224                 featureManager =
225                     FeatureManager(
226                         provideTestConfigurationFlow(scope = scope.backgroundScope),
227                         scope,
228                         setOf(), // Don't register CloudMediaFeature
229                         setOf<RegisteredEventClass>(),
230                         setOf<RegisteredEventClass>(),
231                     ),
232             )
233 
234         testContentProvider.providers =
235             mutableListOf(
236                 Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
237                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
238             )
239 
240         val emissions = mutableListOf<List<Provider>>()
241         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
242         advanceTimeBy(100)
243 
244         assertThat(emissions.count()).isEqualTo(2)
245 
246         // The first emission will be an empty list.
247         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
248 
249         // The next emission will happen once Media Provider responds with the result of
250         // available providers at the time of init. Check that the provider with MediaSource.REMOTE
251         // is not part of the available providers.
252         assertThat(emissions.get(1).count()).isEqualTo(1)
253         assertThat(emissions.get(1).get(0).authority).isEqualTo("local_authority")
254     }
255 
256     @Test
<lambda>null257     fun testAvailableProvidersWhenUserChanges() = runTest {
258         val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
259 
260         val dataService: DataService =
261             DataServiceImpl(
262                 userStatus = userStatusFlow,
263                 scope = this.backgroundScope,
264                 notificationService = notificationService,
265                 mediaProviderClient = mediaProviderClient,
266                 dispatcher = StandardTestDispatcher(this.testScheduler),
267                 config = provideTestConfigurationFlow(this.backgroundScope),
268                 featureManager = testFeatureManager,
269             )
270 
271         val emissions = mutableListOf<List<Provider>>()
272         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
273         advanceTimeBy(100)
274 
275         assertThat(emissions.count()).isEqualTo(2)
276         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
277 
278         // A new user becomes active.
279         userStatusFlow.update {
280             it.copy(allProfiles = listOf(userProfilePrimary, userProfileManaged))
281         }
282 
283         advanceTimeBy(100)
284 
285         // Since the active user did not change, no change should be observed in available
286         // providers.
287         assertThat(emissions.count()).isEqualTo(2)
288 
289         // The active user changes
290         val updatedContentProvider = TestMediaProvider()
291         val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
292         updatedContentProvider.providers =
293             mutableListOf(
294                 Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
295                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
296             )
297 
298         userStatusFlow.update {
299             it.copy(
300                 activeUserProfile = userProfileManaged,
301                 activeContentResolver = updatedContentResolver
302             )
303         }
304 
305         advanceTimeBy(100)
306 
307         // Since the active user has changed, this should trigger a re-fetch of the active
308         // providers.
309         assertThat(emissions.count()).isEqualTo(3)
310 
311         // The first emission will be an empty list.
312         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
313 
314         // The next emission will happen once Media Provider responds with the result of
315         // available providers at the time of init. This will be the last emission from the previous
316         // content provider.
317         assertThat(emissions.get(1).count()).isEqualTo(1)
318         assertThat(emissions.get(1).get(0).authority)
319             .isEqualTo(testContentProvider.providers[0].authority)
320 
321         // The next emission happens when a change in active user is observed. This last emission
322         // should come from the updated content provider.
323         assertThat(emissions.get(2).count()).isEqualTo(2)
324         assertThat(emissions.get(2).get(0).authority)
325             .isEqualTo(updatedContentProvider.providers[0].authority)
326         assertThat(emissions.get(2).get(1).authority)
327             .isEqualTo(updatedContentProvider.providers[1].authority)
328     }
329 
330     @Test
<lambda>null331     fun testContentObserverRegistrationWhenUserChanges() = runTest {
332         val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
333         val mockNotificationService = mock(NotificationService::class.java)
334 
335         val dataService: DataService =
336             DataServiceImpl(
337                 userStatus = userStatusFlow,
338                 scope = this.backgroundScope,
339                 notificationService = mockNotificationService,
340                 mediaProviderClient = mediaProviderClient,
341                 dispatcher = StandardTestDispatcher(this.testScheduler),
342                 config = provideTestConfigurationFlow(this.backgroundScope),
343                 featureManager = testFeatureManager,
344             )
345 
346         val emissions = mutableListOf<List<Provider>>()
347         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
348         advanceTimeBy(100)
349 
350         // Verify initial available provider emissions.
351         assertThat(emissions.count()).isEqualTo(2)
352         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
353 
354         val defaultContentObserver: ContentObserver =
355             object : ContentObserver(/* handler */ null) {
356                 override fun onChange(selfChange: Boolean, uri: Uri?) {}
357             }
358 
359         verify(mockNotificationService)
360             .registerContentObserverCallback(
361                 nonNullableEq(testContentResolver),
362                 nonNullableEq(availableProvidersUpdateUri),
363                 ArgumentMatchers.eq(true),
364                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
365             )
366 
367         verify(mockNotificationService)
368             .registerContentObserverCallback(
369                 nonNullableEq(testContentResolver),
370                 nonNullableEq(mediaUpdateUri),
371                 ArgumentMatchers.eq(true),
372                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
373             )
374 
375         verify(mockNotificationService)
376             .registerContentObserverCallback(
377                 nonNullableEq(testContentResolver),
378                 nonNullableEq(albumMediaUpdateUri),
379                 ArgumentMatchers.eq(true),
380                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
381             )
382 
383         // Change the active user
384         val updatedContentProvider = TestMediaProvider()
385         val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
386         updatedContentProvider.providers =
387             mutableListOf(
388                 Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
389                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
390             )
391 
392         userStatusFlow.update {
393             it.copy(
394                 activeUserProfile = userProfileManaged,
395                 activeContentResolver = updatedContentResolver
396             )
397         }
398 
399         advanceTimeBy(100)
400 
401         verify(mockNotificationService, times(3))
402             .unregisterContentObserverCallback(
403                 nonNullableEq(testContentResolver),
404                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
405             )
406 
407         verify(mockNotificationService)
408             .registerContentObserverCallback(
409                 nonNullableEq(updatedContentResolver),
410                 nonNullableEq(availableProvidersUpdateUri),
411                 ArgumentMatchers.eq(true),
412                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
413             )
414 
415         verify(mockNotificationService)
416             .registerContentObserverCallback(
417                 nonNullableEq(updatedContentResolver),
418                 nonNullableEq(mediaUpdateUri),
419                 ArgumentMatchers.eq(true),
420                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
421             )
422 
423         verify(mockNotificationService)
424             .registerContentObserverCallback(
425                 nonNullableEq(updatedContentResolver),
426                 nonNullableEq(albumMediaUpdateUri),
427                 ArgumentMatchers.eq(true),
428                 nonNullableAny(ContentObserver::class.java, defaultContentObserver)
429             )
430     }
431 
432     @Test
<lambda>null433     fun testMediaPagingSourceInvalidation() = runTest {
434         val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
435 
436         val dataService: DataService =
437             DataServiceImpl(
438                 userStatus = userStatusFlow,
439                 scope = this.backgroundScope,
440                 notificationService = notificationService,
441                 mediaProviderClient = mediaProviderClient,
442                 dispatcher = StandardTestDispatcher(this.testScheduler),
443                 config = provideTestConfigurationFlow(this.backgroundScope),
444                 featureManager = testFeatureManager,
445             )
446 
447         val emissions = mutableListOf<List<Provider>>()
448         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
449         advanceTimeBy(100)
450 
451         assertThat(emissions.count()).isEqualTo(2)
452         assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
453 
454         val firstMediaPagingSource: PagingSource<MediaPageKey, Media> =
455             dataService.mediaPagingSource()
456         assertThat(firstMediaPagingSource.invalid).isFalse()
457 
458         // The active user changes
459         val updatedContentProvider = TestMediaProvider()
460         val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
461         updatedContentProvider.providers =
462             mutableListOf(
463                 Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
464                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
465             )
466 
467         userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
468 
469         advanceTimeBy(1000)
470 
471         // Since the active user has changed, this should trigger a re-fetch of the active
472         // providers.
473         assertThat(emissions.count()).isEqualTo(3)
474 
475         // Check that the previously created MediaPagingSource has been invalidated.
476         assertThat(firstMediaPagingSource.invalid).isTrue()
477 
478         // Check that the new MediaPagingSource instance is still valid.
479         val secondMediaPagingSource: PagingSource<MediaPageKey, Media> =
480             dataService.mediaPagingSource()
481         assertThat(secondMediaPagingSource.invalid).isFalse()
482     }
483 
484     @Test
<lambda>null485     fun testAlbumPagingSourceInvalidation() = runTest {
486         val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
487 
488         val dataService: DataService =
489             DataServiceImpl(
490                 userStatus = userStatusFlow,
491                 scope = this.backgroundScope,
492                 notificationService = notificationService,
493                 mediaProviderClient = mediaProviderClient,
494                 dispatcher = StandardTestDispatcher(this.testScheduler),
495                 config = provideTestConfigurationFlow(this.backgroundScope),
496                 featureManager = testFeatureManager,
497             )
498 
499         // Check initial available provider emissions
500         val emissions = mutableListOf<List<Provider>>()
501         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
502         advanceTimeBy(100)
503 
504         assertThat(emissions.count()).isEqualTo(2)
505         assertThat(emissions[0]).isEqualTo(emptyList<Provider>())
506 
507         val firstAlbumPagingSource: PagingSource<MediaPageKey, Group.Album> =
508             dataService.albumPagingSource()
509         assertThat(firstAlbumPagingSource.invalid).isFalse()
510 
511         // The active user changes
512         val updatedContentProvider = TestMediaProvider()
513         val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
514         updatedContentProvider.providers =
515             mutableListOf(
516                 Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
517                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
518             )
519 
520         userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
521         advanceTimeBy(100)
522 
523         // Since the active user has changed, this should trigger a re-fetch of the active
524         // providers.
525         assertThat(emissions.count()).isEqualTo(3)
526 
527         // Check that the previously created MediaPagingSource has been invalidated.
528         assertThat(firstAlbumPagingSource.invalid).isTrue()
529 
530         // Check that the new MediaPagingSource instance is still valid.
531         val secondAlbumPagingSource: PagingSource<MediaPageKey, Group.Album> =
532             dataService.albumPagingSource()
533         assertThat(secondAlbumPagingSource.invalid).isFalse()
534     }
535 
536     @Test
testAlbumMediaPagingSourceCacheUpdatesnull537     fun testAlbumMediaPagingSourceCacheUpdates() = runTest {
538         testContentProvider.lastRefreshMediaRequest = null
539 
540         val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
541         val dataService: DataService =
542             DataServiceImpl(
543                 userStatus = userStatusFlow,
544                 scope = this.backgroundScope,
545                 notificationService = notificationService,
546                 mediaProviderClient = mediaProviderClient,
547                 dispatcher = StandardTestDispatcher(this.testScheduler),
548                 config = provideTestConfigurationFlow(this.backgroundScope),
549                 featureManager = testFeatureManager,
550             )
551         advanceTimeBy(100)
552 
553         // Fetch album media the first time
554         val albumId = testContentProvider.albumMedia.keys.first()
555         val album =
556             Group.Album(
557                 id = albumId,
558                 pickerId = Long.MAX_VALUE,
559                 authority = testContentProvider.providers[0].authority,
560                 dateTakenMillisLong = Long.MAX_VALUE,
561                 displayName = "album",
562                 coverUri = Uri.parse("content://media/picker/authority/media/${Long.MAX_VALUE}"),
563                 coverMediaSource = testContentProvider.providers[0].mediaSource
564             )
565 
566         val firstAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
567             dataService.albumMediaPagingSource(album)
568 
569         // Check the album media paging source is valid
570         assertThat(firstAlbumMediaPagingSource.invalid).isFalse()
571 
572         // Check that a cache refresh request was received
573         val albumMediaRefreshRequestExtras = testContentProvider.lastRefreshMediaRequest
574         assertThat(albumMediaRefreshRequestExtras).isNotNull()
575 
576         // Fetch the album media again
577         val secondAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
578             dataService.albumMediaPagingSource(album)
579 
580         // Check the previous album media source was reused because it was not marked as invalid.
581         assertThat(secondAlbumMediaPagingSource.invalid).isFalse()
582         assertThat(secondAlbumMediaPagingSource).isEqualTo(firstAlbumMediaPagingSource)
583 
584         // Check that a cache refresh request was not received the second time
585         assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull()
586         assertThat(testContentProvider.lastRefreshMediaRequest)
587             .isEqualTo(albumMediaRefreshRequestExtras)
588 
589         // Mark the paging source as invalid
590         secondAlbumMediaPagingSource.invalidate()
591 
592         // Fetch the album media again
593         val thirdAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
594             dataService.albumMediaPagingSource(album)
595 
596         // Check the previous album media source was not reused because it was invalidated.
597         assertThat(secondAlbumMediaPagingSource.invalid).isTrue()
598         assertThat(thirdAlbumMediaPagingSource.invalid).isFalse()
599         assertThat(thirdAlbumMediaPagingSource).isNotEqualTo(secondAlbumMediaPagingSource)
600 
601         // Check that a cache refresh request was not received the third time either
602         assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull()
603         assertThat(testContentProvider.lastRefreshMediaRequest)
604             .isEqualTo(albumMediaRefreshRequestExtras)
605     }
606 
607     @Test
<lambda>null608     fun testAlbumMediaPagingSourceInvalidation() = runTest {
609         val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
610 
611         val dataService: DataService =
612             DataServiceImpl(
613                 userStatus = userStatusFlow,
614                 scope = this.backgroundScope,
615                 notificationService = notificationService,
616                 mediaProviderClient = mediaProviderClient,
617                 dispatcher = StandardTestDispatcher(this.testScheduler),
618                 config = provideTestConfigurationFlow(this.backgroundScope),
619                 featureManager = testFeatureManager,
620             )
621 
622         // Check initial available provider emissions
623         val emissions = mutableListOf<List<Provider>>()
624         this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
625         advanceTimeBy(100)
626 
627         assertThat(emissions.count()).isEqualTo(2)
628         assertThat(emissions[0]).isEqualTo(emptyList<Provider>())
629 
630         // Fetch album media the first time
631         val albumId = testContentProvider.albumMedia.keys.first()
632         val album =
633             Group.Album(
634                 id = albumId,
635                 pickerId = Long.MAX_VALUE,
636                 authority = testContentProvider.providers[0].authority,
637                 dateTakenMillisLong = Long.MAX_VALUE,
638                 displayName = "album",
639                 coverUri = Uri.parse("content://media/picker/authority/media/${Long.MAX_VALUE}"),
640                 coverMediaSource = testContentProvider.providers[0].mediaSource
641             )
642 
643         val firstAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
644             dataService.albumMediaPagingSource(album)
645 
646         // Check the album media paging source is valid
647         assertThat(firstAlbumMediaPagingSource.invalid).isFalse()
648 
649         // Check that a cache refresh request was received
650         val firstAlbumMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
651         assertThat(firstAlbumMediaRefreshRequest).isNotNull()
652 
653         // The active user changes
654         val updatedContentProvider = TestMediaProvider()
655         val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
656         updatedContentProvider.providers =
657             mutableListOf(
658                 Provider(
659                     authority = testContentProvider.providers[0].authority,
660                     mediaSource = MediaSource.LOCAL,
661                     uid = 0
662                 ),
663                 Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
664             )
665 
666         userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
667         advanceTimeBy(100)
668 
669         // Since the active user has changed, this should trigger a re-fetch of the active
670         // providers.
671         assertThat(emissions.count()).isEqualTo(3)
672 
673         // Fetch the album media again
674         val secondAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
675             dataService.albumMediaPagingSource(album)
676 
677         // Check that previous album media source was marked as invalid.
678         assertThat(firstAlbumMediaPagingSource.invalid).isTrue()
679         assertThat(secondAlbumMediaPagingSource.invalid).isFalse()
680         assertThat(secondAlbumMediaPagingSource).isNotEqualTo(firstAlbumMediaPagingSource)
681 
682         // Check that a cache refresh request was received again because the album media paging
683         // source cache was cleared.
684         val secondAlbumMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
685         assertThat(secondAlbumMediaPagingSource).isNotNull()
686         assertThat(secondAlbumMediaPagingSource).isNotEqualTo(secondAlbumMediaRefreshRequest)
687     }
688 
689     @Test
<lambda>null690     fun testOnUpdateMediaNotification() = runTest {
691         val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
692 
693         val dataService: DataService =
694             DataServiceImpl(
695                 userStatus = userStatusFlow,
696                 scope = this.backgroundScope,
697                 notificationService = notificationService,
698                 mediaProviderClient = mediaProviderClient,
699                 dispatcher = StandardTestDispatcher(this.testScheduler),
700                 config = provideTestConfigurationFlow(this.backgroundScope),
701                 featureManager = testFeatureManager,
702             )
703         advanceTimeBy(100)
704 
705         val firstMediaPagingSource: PagingSource<MediaPageKey, Media> =
706             dataService.mediaPagingSource()
707         assertThat(firstMediaPagingSource.invalid).isFalse()
708 
709         // Check that a cache refresh request was received
710         val firstMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
711         assertThat(firstMediaRefreshRequest).isNotNull()
712 
713         // Send a media update notification
714         notificationService.dispatchChangeToObservers(mediaUpdateUri)
715         advanceTimeBy(100)
716 
717         // Check that the first media paging source was marked as invalid
718         assertThat(firstMediaPagingSource.invalid).isTrue()
719 
720         // Check that the a new PagingSource instance was created which is still valid
721         val secondMediaPagingSource: PagingSource<MediaPageKey, Media> =
722             dataService.mediaPagingSource()
723         assertThat(secondMediaPagingSource).isNotEqualTo(firstMediaPagingSource)
724         assertThat(secondMediaPagingSource.invalid).isFalse()
725 
726         // Check that a cache update request was not received a second time
727         val lastMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
728         assertThat(lastMediaRefreshRequest).isEqualTo(firstMediaRefreshRequest)
729     }
730 
731     @Test
<lambda>null732     fun testOnUpdateAlbumMediaNotification() = runTest {
733         val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
734 
735         val dataService: DataService =
736             DataServiceImpl(
737                 userStatus = userStatusFlow,
738                 scope = this.backgroundScope,
739                 notificationService = notificationService,
740                 mediaProviderClient = mediaProviderClient,
741                 dispatcher = StandardTestDispatcher(this.testScheduler),
742                 config = provideTestConfigurationFlow(this.backgroundScope),
743                 featureManager = testFeatureManager,
744             )
745         advanceTimeBy(100)
746 
747         // Fetch album media the first time
748         val album =
749             Group.Album(
750                 id = testContentProvider.albumMedia.keys.first(),
751                 pickerId = Long.MAX_VALUE,
752                 authority = testContentProvider.providers[0].authority,
753                 dateTakenMillisLong = Long.MAX_VALUE,
754                 displayName = "album",
755                 coverUri = Uri.parse("content://media/picker/authority/media/${Long.MAX_VALUE}"),
756                 coverMediaSource = testContentProvider.providers[0].mediaSource
757             )
758 
759         val firstAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
760             dataService.albumMediaPagingSource(album)
761 
762         // Check the album media paging source is valid
763         assertThat(firstAlbumMediaPagingSource.invalid).isFalse()
764 
765         // Check that a cache refresh request was received
766         val firstAlbumMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
767         assertThat(firstAlbumMediaRefreshRequest).isNotNull()
768 
769         // Send a media update notification
770         val albumUpdateUri: Uri =
771             albumMediaUpdateUri
772                 .buildUpon()
773                 .apply {
774                     appendPath(album.authority)
775                     appendPath(album.id)
776                 }
777                 .build()
778 
779         notificationService.dispatchChangeToObservers(albumUpdateUri)
780         advanceTimeBy(100)
781 
782         // Check that the first media paging source was marked as invalid
783         assertThat(firstAlbumMediaPagingSource.invalid).isTrue()
784 
785         // Check that the a new PagingSource instance was created which is still valid
786         val secondAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
787             dataService.albumMediaPagingSource(album)
788         assertThat(secondAlbumMediaPagingSource).isNotEqualTo(firstAlbumMediaPagingSource)
789         assertThat(secondAlbumMediaPagingSource.invalid).isFalse()
790 
791         // Check that a cache update request was not received a second time
792         val lastAlbumMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
793         assertThat(lastAlbumMediaRefreshRequest).isEqualTo(firstAlbumMediaRefreshRequest)
794     }
795 }
796