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