1 /* <lambda>null2 * 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.data 18 19 import android.content.ContentResolver 20 import android.content.Context 21 import android.content.Intent 22 import android.content.pm.ResolveInfo 23 import android.database.ContentObserver 24 import android.net.Uri 25 import android.os.UserHandle 26 import android.provider.CloudMediaProviderContract 27 import android.provider.MediaStore 28 import android.util.Log 29 import androidx.annotation.GuardedBy 30 import androidx.paging.PagingSource 31 import com.android.photopicker.core.configuration.PhotopickerConfiguration 32 import com.android.photopicker.core.events.Events 33 import com.android.photopicker.core.features.FeatureManager 34 import com.android.photopicker.core.user.UserStatus 35 import com.android.photopicker.data.model.CloudMediaProviderDetails 36 import com.android.photopicker.data.model.CollectionInfo 37 import com.android.photopicker.data.model.Group.Album 38 import com.android.photopicker.data.model.Media 39 import com.android.photopicker.data.model.MediaPageKey 40 import com.android.photopicker.data.model.MediaSource 41 import com.android.photopicker.data.model.Provider 42 import com.android.photopicker.data.paging.AlbumMediaPagingSource 43 import com.android.photopicker.data.paging.AlbumPagingSource 44 import com.android.photopicker.data.paging.MediaPagingSource 45 import com.android.photopicker.features.cloudmedia.CloudMediaFeature 46 import kotlinx.coroutines.CoroutineDispatcher 47 import kotlinx.coroutines.CoroutineScope 48 import kotlinx.coroutines.Job 49 import kotlinx.coroutines.channels.Channel 50 import kotlinx.coroutines.channels.Channel.Factory.CONFLATED 51 import kotlinx.coroutines.channels.awaitClose 52 import kotlinx.coroutines.flow.Flow 53 import kotlinx.coroutines.flow.MutableStateFlow 54 import kotlinx.coroutines.flow.SharingStarted 55 import kotlinx.coroutines.flow.StateFlow 56 import kotlinx.coroutines.flow.callbackFlow 57 import kotlinx.coroutines.flow.map 58 import kotlinx.coroutines.flow.stateIn 59 import kotlinx.coroutines.flow.update 60 import kotlinx.coroutines.launch 61 import kotlinx.coroutines.runBlocking 62 import kotlinx.coroutines.sync.Mutex 63 import kotlinx.coroutines.sync.withLock 64 65 /** 66 * Provides data to the Photo Picker UI. The data comes from a [ContentProvider] called 67 * [MediaProvider]. 68 * 69 * Underlying data changes in [MediaProvider] are observed using [ContentObservers]. When a change 70 * in data is observed, the data is re-fetched from the [MediaProvider] process and the new data is 71 * emitted to the [StateFlows]-s. 72 * 73 * @param userStatus A [StateFlow] with the current active user's details. 74 * @param scope The [CoroutineScope] the data flows will be shared in. 75 * @param dispatcher A [CoroutineDispatcher] to run the coroutines in. 76 * @param notificationService An instance of [NotificationService] responsible to listen to data 77 * change notifications. 78 * @param mediaProviderClient An instance of [MediaProviderClient] responsible to get data from 79 * MediaProvider. 80 * @param config [StateFlow] that emits [PhotopickerConfiguration] changes. 81 */ 82 class DataServiceImpl( 83 private val userStatus: StateFlow<UserStatus>, 84 private val scope: CoroutineScope, 85 private val dispatcher: CoroutineDispatcher, 86 private val notificationService: NotificationService, 87 private val mediaProviderClient: MediaProviderClient, 88 private val config: StateFlow<PhotopickerConfiguration>, 89 private val featureManager: FeatureManager, 90 private val appContext: Context, 91 private val events: Events, 92 private val processOwnerHandle: UserHandle, 93 ) : DataService { 94 // Here default value being null signifies that the look up for the grants has not happened yet. 95 // Use [refreshPreGrantedItemsCount] to populate this with the latest value. 96 private var _preGrantedMediaCount: MutableStateFlow<Int?> = MutableStateFlow(null) 97 98 // Here default value being null signifies that the look up for the uris has not happened yet. 99 // Use [fetchMediaDataForUris] to populate this with the latest value. 100 private var _preSelectionMediaData: MutableStateFlow<List<Media>?> = MutableStateFlow(null) 101 102 // Keep track of the photo grid media, album grid and preview media paging sources so that we 103 // can invalidate them in case the underlying data changes. 104 private val mediaPagingSources: MutableList<MediaPagingSource> = mutableListOf() 105 private val albumPagingSources: MutableList<AlbumPagingSource> = mutableListOf() 106 107 // Keep track of the album grid media paging sources so that we can invalidate 108 // them in case the underlying data changes or re-use them if the user re-opens the same album 109 // again. If something drastically changes that would require a refresh of the data source 110 // cache, remove the paging source from the below map. If a paging source is found the in map, 111 // it is assumed that a refresh request was already sent to the data source once in the session 112 // and there is no need to send it again, even if the paging source is invalid. 113 private val albumMediaPagingSources: 114 MutableMap<String, MutableMap<String, AlbumMediaPagingSource>> = 115 mutableMapOf() 116 117 // An internal lock to allow thread-safe updates to the [MediaPagingSource] and 118 // [AlbumPagingSource]. 119 private val mediaPagingSourceMutex = Mutex() 120 121 // An internal lock to allow thread-safe updates to the [AlbumMediaPagingSource]. 122 private val albumMediaPagingSourceMutex = Mutex() 123 124 /** 125 * Callback flow that listens to changes in the available providers and emits updated list of 126 * available providers. 127 */ 128 private var availableProviderCallbackFlow: Flow<List<Provider>>? = null 129 130 /** 131 * Callback flow that listens to changes in media and emits a [Unit] when change is observed. 132 */ 133 private var mediaUpdateCallbackFlow: Flow<Unit>? = null 134 135 /** 136 * Callback flow that listens to changes in album media and emits a [Pair] of album authority 137 * and album id when change is observed. 138 */ 139 private var albumMediaUpdateCallbackFlow: Flow<Pair<String, String>>? = null 140 141 /** 142 * Saves the current job that collects the [availableProviderCallbackFlow]. Cancel this job when 143 * there is a change in the [activeContentResolver] 144 */ 145 private var availableProviderCollectJob: Job? = null 146 147 /** 148 * Saves the current job that collects the [mediaUpdateCallbackFlow]. Cancel this job when there 149 * is a change in the [activeContentResolver] 150 */ 151 private var mediaUpdateCollectJob: Job? = null 152 153 /** 154 * Saves the current job that collects the [albumMediaUpdateCallbackFlow]. Cancel this job when 155 * there is a change in the [activeContentResolver] 156 */ 157 private var albumMediaUpdateCollectJob: Job? = null 158 159 /** 160 * Internal [StateFlow] that emits when the [availableProviderCallbackFlow] emits a new list of 161 * providers. The [availableProviderCallbackFlow] can change if the active user in a session has 162 * changed. 163 * 164 * This flow is directly initialized with the available providers fetched from the data source 165 * because if we initialize with a default empty list here, all PagingSource objects will get 166 * created with an empty provider list and result in a transient error state. 167 */ 168 private val _availableProviders: MutableStateFlow<List<Provider>> by lazy { 169 MutableStateFlow(fetchAvailableProviders()) 170 } 171 172 override val activeContentResolver = 173 MutableStateFlow<ContentResolver>(userStatus.value.activeContentResolver) 174 175 /** 176 * Create an immutable state flow from the callback flow [_availableProviders]. The state flow 177 * helps retain and provide immediate access to the last emitted value. 178 * 179 * The producer block remains active for some time after the last observer stops collecting. 180 * This helps retain the flow through transient changes like activity recreation due to config 181 * changes. 182 * 183 * Note that [StateFlow] automatically filters out subsequent repetitions of the same value. 184 */ 185 override val availableProviders: StateFlow<List<Provider>> = 186 _availableProviders.stateIn( 187 scope, 188 SharingStarted.WhileSubscribed(FLOW_TIMEOUT_MILLI_SECONDS), 189 _availableProviders.value, 190 ) 191 192 // Contains collection info cache 193 private val collectionInfoState = 194 CollectionInfoState(mediaProviderClient, activeContentResolver, availableProviders) 195 196 override val disruptiveDataUpdateChannel = Channel<Unit>(CONFLATED) 197 198 /** 199 * Same as [_preGrantedMediaCount] but as an immutable StateFlow. The count contains the latest 200 * value set during the most recent [refreshPreGrantedItemsCount] call. 201 */ 202 override val preGrantedMediaCount: StateFlow<Int?> = _preGrantedMediaCount 203 204 /** 205 * Same as [_preSelectionMediaData] but as an immutable StateFlow. The flow contains the latest 206 * value set during the most recent [fetchMediaDataForUris] call. 207 */ 208 override val preSelectionMediaData: StateFlow<List<Media>?> = _preSelectionMediaData 209 210 companion object { 211 const val FLOW_TIMEOUT_MILLI_SECONDS: Long = 5000 212 } 213 214 init { 215 scope.launch(dispatcher) { 216 availableProviders.collect { providers: List<Provider> -> 217 Log.d(DataService.TAG, "Available providers have changed to $providers.") 218 219 mediaPagingSourceMutex.withLock { 220 mediaPagingSources.forEach { mediaPagingSource -> 221 mediaPagingSource.invalidate() 222 } 223 albumPagingSources.forEach { albumPagingSource -> 224 albumPagingSource.invalidate() 225 } 226 227 mediaPagingSources.clear() 228 albumPagingSources.clear() 229 } 230 231 albumMediaPagingSourceMutex.withLock { 232 albumMediaPagingSources.values.forEach { albumMediaPagingSourceMap -> 233 albumMediaPagingSourceMap.values.forEach { albumMediaPagingSource -> 234 albumMediaPagingSource.invalidate() 235 } 236 } 237 albumMediaPagingSources.clear() 238 } 239 } 240 } 241 242 scope.launch(dispatcher) { 243 // Only observe the changes in the active content resolver 244 activeContentResolver.collect { activeContentResolver: ContentResolver -> 245 Log.d(DataService.TAG, "Active content resolver has changed.") 246 247 // Stop collecting available providers from previously initialized callback flow. 248 availableProviderCollectJob?.cancel() 249 availableProviderCallbackFlow = initAvailableProvidersFlow(activeContentResolver) 250 251 availableProviderCollectJob = 252 scope.launch(dispatcher) { 253 availableProviderCallbackFlow?.collect { providers: List<Provider> -> 254 Log.d( 255 DataService.TAG, 256 "Available providers update notification received $providers", 257 ) 258 259 updateAvailableProviders(providers) 260 } 261 } 262 263 // Stop collecting media updates from previously initialized callback flow. 264 mediaUpdateCollectJob?.cancel() 265 mediaUpdateCallbackFlow = initMediaUpdateFlow(activeContentResolver) 266 267 mediaUpdateCollectJob = 268 scope.launch(dispatcher) { 269 mediaUpdateCallbackFlow?.collect { 270 Log.d(DataService.TAG, "Media update notification received") 271 mediaPagingSourceMutex.withLock { 272 mediaPagingSources.forEach { mediaPagingSource -> 273 mediaPagingSource.invalidate() 274 } 275 } 276 } 277 } 278 279 // Stop collecting album media updates from previously initialized callback flow. 280 albumMediaUpdateCollectJob?.cancel() 281 albumMediaUpdateCallbackFlow = initAlbumMediaUpdateFlow(activeContentResolver) 282 283 albumMediaUpdateCollectJob = 284 scope.launch(dispatcher) { 285 albumMediaUpdateCallbackFlow?.collect { 286 (albumAuthority, albumId): Pair<String, String> -> 287 Log.d( 288 DataService.TAG, 289 "Album media update notification " + 290 "received for album authority $albumAuthority " + 291 "and album id $albumId", 292 ) 293 albumMediaPagingSourceMutex.withLock { 294 albumMediaPagingSources 295 .get(albumAuthority) 296 ?.get(albumId) 297 ?.invalidate() 298 } 299 } 300 } 301 } 302 } 303 304 scope.launch(dispatcher) { 305 userStatus.collect { userStatusValue: UserStatus -> 306 activeContentResolver.update { userStatusValue.activeContentResolver } 307 } 308 } 309 } 310 311 /** 312 * Creates a callback flow that listens to changes in the available providers using 313 * [ContentObserver] and emits updated list of available providers. 314 */ 315 private fun initAvailableProvidersFlow(resolver: ContentResolver): Flow<List<Provider>> = 316 callbackFlow<Unit> { 317 // Define a callback that tries sending a [Unit] in the [Channel]. 318 val observer = 319 object : ContentObserver(/* handler */ null) { 320 override fun onChange(selfChange: Boolean, uri: Uri?) { 321 trySend(Unit) 322 } 323 } 324 325 // Register the content observer callback. 326 notificationService.registerContentObserverCallback( 327 resolver, 328 AVAILABLE_PROVIDERS_CHANGE_NOTIFICATION_URI, 329 /* notifyForDescendants */ true, 330 observer, 331 ) 332 333 // Trigger the first fetch of available providers. 334 trySend(Unit) 335 336 // Unregister when the flow is closed. 337 awaitClose { 338 notificationService.unregisterContentObserverCallback(resolver, observer) 339 } 340 } 341 .map { 342 // Fetch the available providers again when a change is detected. 343 fetchAvailableProviders() 344 } 345 346 /** 347 * Creates a callback flow that emits a [Unit] when an update in media is observed using 348 * [ContentObserver] notifications. 349 */ 350 private fun initMediaUpdateFlow(resolver: ContentResolver): Flow<Unit> = 351 callbackFlow<Unit> { 352 val observer = 353 object : ContentObserver(/* handler */ null) { 354 override fun onChange(selfChange: Boolean, uri: Uri?) { 355 trySend(Unit) 356 } 357 } 358 359 // Register the content observer callback. 360 notificationService.registerContentObserverCallback( 361 resolver, 362 MEDIA_CHANGE_NOTIFICATION_URI, 363 /* notifyForDescendants */ true, 364 observer, 365 ) 366 367 // Unregister when the flow is closed. 368 awaitClose { notificationService.unregisterContentObserverCallback(resolver, observer) } 369 } 370 371 /** 372 * Creates a callback flow that emits the album ID when an update in the album's media is 373 * observed using [ContentObserver] notifications. 374 */ 375 private fun initAlbumMediaUpdateFlow(resolver: ContentResolver): Flow<Pair<String, String>> = 376 callbackFlow { 377 val observer = 378 object : ContentObserver(/* handler */ null) { 379 override fun onChange(selfChange: Boolean, uri: Uri?) { 380 // Verify that album authority and album ID is present in the URI 381 if ( 382 uri?.pathSegments?.size == 383 (2 + ALBUM_CHANGE_NOTIFICATION_URI.pathSegments.size) 384 ) { 385 val albumAuthority = uri.pathSegments[uri.pathSegments.size - 2] ?: "" 386 val albumID = uri.pathSegments[uri.pathSegments.size - 1] ?: "" 387 trySend(Pair(albumAuthority, albumID)) 388 } 389 } 390 } 391 392 // Register the content observer callback. 393 notificationService.registerContentObserverCallback( 394 resolver, 395 ALBUM_CHANGE_NOTIFICATION_URI, 396 /* notifyForDescendants */ true, 397 observer, 398 ) 399 400 // Unregister when the flow is closed. 401 awaitClose { notificationService.unregisterContentObserverCallback(resolver, observer) } 402 } 403 404 @GuardedBy("albumMediaPagingSourceMutex") 405 override fun albumMediaPagingSource(album: Album): PagingSource<MediaPageKey, Media> = 406 runBlocking { 407 refreshAlbumMedia(album) 408 409 albumMediaPagingSourceMutex.withLock { 410 val albumMap = albumMediaPagingSources.getOrDefault(album.authority, mutableMapOf()) 411 412 if (!albumMap.containsKey(album.id) || albumMap[album.id]!!.invalid) { 413 val availableProviders: List<Provider> = availableProviders.value 414 val contentResolver: ContentResolver = activeContentResolver.value 415 val albumMediaPagingSource = 416 AlbumMediaPagingSource( 417 album.id, 418 album.authority, 419 contentResolver, 420 availableProviders, 421 mediaProviderClient, 422 dispatcher, 423 config.value, 424 events, 425 ) 426 427 Log.v( 428 DataService.TAG, 429 "Created an album media paging source that queries $availableProviders", 430 ) 431 432 albumMap[album.id] = albumMediaPagingSource 433 albumMediaPagingSources[album.authority] = albumMap 434 } 435 436 albumMap[album.id]!! 437 } 438 } 439 440 @GuardedBy("mediaPagingSourceMutex") 441 override fun albumPagingSource(): PagingSource<MediaPageKey, Album> = runBlocking { 442 mediaPagingSourceMutex.withLock { 443 val availableProviders: List<Provider> = availableProviders.value 444 val contentResolver: ContentResolver = activeContentResolver.value 445 val albumPagingSource = 446 AlbumPagingSource( 447 contentResolver, 448 availableProviders, 449 mediaProviderClient, 450 dispatcher, 451 config.value, 452 events, 453 ) 454 455 Log.v( 456 DataService.TAG, 457 "Created an album paging source that queries $availableProviders", 458 ) 459 460 albumPagingSources.add(albumPagingSource) 461 albumPagingSource 462 } 463 } 464 465 override fun cloudMediaProviderDetails( 466 authority: String 467 ): StateFlow<CloudMediaProviderDetails?> = 468 throw NotImplementedError("This method is not implemented yet.") 469 470 @GuardedBy("mediaPagingSourceMutex") 471 override fun mediaPagingSource(): PagingSource<MediaPageKey, Media> = runBlocking { 472 mediaPagingSourceMutex.withLock { 473 val availableProviders: List<Provider> = availableProviders.value 474 val contentResolver: ContentResolver = activeContentResolver.value 475 val mediaPagingSource = 476 MediaPagingSource( 477 contentResolver, 478 availableProviders, 479 mediaProviderClient, 480 dispatcher, 481 config.value, 482 events, 483 ) 484 485 Log.v(DataService.TAG, "Created a media paging source that queries $availableProviders") 486 487 mediaPagingSources.add(mediaPagingSource) 488 mediaPagingSource 489 } 490 } 491 492 @GuardedBy("mediaPagingSourceMutex") 493 override fun previewMediaPagingSource( 494 currentSelection: Set<Media>, 495 currentDeselection: Set<Media>, 496 ): PagingSource<MediaPageKey, Media> = runBlocking { 497 mediaPagingSourceMutex.withLock { 498 val availableProviders: List<Provider> = availableProviders.value 499 val contentResolver: ContentResolver = activeContentResolver.value 500 val mediaPagingSource = 501 MediaPagingSource( 502 contentResolver, 503 availableProviders, 504 mediaProviderClient, 505 dispatcher, 506 config.value, 507 events, 508 /* is_preview_request */ true, 509 currentSelection.mapNotNull { it.mediaId }.toCollection(ArrayList()), 510 currentDeselection.mapNotNull { it.mediaId }.toCollection(ArrayList()), 511 ) 512 513 Log.v( 514 DataService.TAG, 515 "Created a media paging source that queries database for preview items.", 516 ) 517 mediaPagingSources.add(mediaPagingSource) 518 mediaPagingSource 519 } 520 } 521 522 override suspend fun refreshMedia() { 523 val availableProviders: List<Provider> = availableProviders.value 524 refreshMedia(availableProviders) 525 } 526 527 @GuardedBy("albumMediaPagingSourceMutex") 528 override suspend fun refreshAlbumMedia(album: Album) { 529 albumMediaPagingSourceMutex.withLock { 530 // Send album media refresh request only when the album media paging source is not 531 // already cached. 532 if ( 533 albumMediaPagingSources.containsKey(album.authority) && 534 albumMediaPagingSources[album.authority]!!.containsKey(album.id) 535 ) { 536 Log.i( 537 DataService.TAG, 538 "A media paging source is available for " + 539 "album ${album.id}. Not sending a refresh album media request.", 540 ) 541 return 542 } 543 } 544 545 val providers = availableProviders.value 546 val isAlbumProviderAvailable = 547 providers.any { provider -> provider.authority == album.authority } 548 549 if (isAlbumProviderAvailable) { 550 mediaProviderClient.refreshAlbumMedia( 551 album.id, 552 album.authority, 553 providers, 554 activeContentResolver.value, 555 config.value, 556 ) 557 } else { 558 Log.e( 559 DataService.TAG, 560 "Available providers $providers " + 561 "does not contain album authority ${album.authority}. " + 562 "Skip sending refresh album media request.", 563 ) 564 } 565 } 566 567 override suspend fun getCollectionInfo(provider: Provider): CollectionInfo { 568 return collectionInfoState.getCollectionInfo(provider) 569 } 570 571 override suspend fun ensureProviders() { 572 mediaProviderClient.ensureProviders(activeContentResolver.value) 573 updateAvailableProviders(fetchAvailableProviders()) 574 } 575 576 override fun getAllAllowedProviders(): List<Provider> { 577 val configSnapshot = config.value 578 val user = userStatus.value.activeUserProfile.handle 579 val enforceAllowlist = configSnapshot.flags.CLOUD_ENFORCE_PROVIDER_ALLOWLIST 580 val allowlist = configSnapshot.flags.CLOUD_ALLOWED_PROVIDERS 581 val intent = Intent(CloudMediaProviderContract.PROVIDER_INTERFACE) 582 val packageManager = appContext.getPackageManager() 583 val allProviders: List<ResolveInfo> = 584 packageManager.queryIntentContentProvidersAsUser(intent, /* flags */ 0, user) 585 586 val allowedProviders = 587 allProviders 588 .filter { 589 it.providerInfo.authority != null && 590 CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals( 591 it.providerInfo.readPermission 592 ) && 593 (!enforceAllowlist || allowlist.contains(it.providerInfo.packageName)) 594 } 595 .map { 596 Provider( 597 authority = it.providerInfo.authority, 598 mediaSource = MediaSource.REMOTE, 599 uid = 600 packageManager.getPackageUid( 601 it.providerInfo.packageName, 602 /* flags */ 0, 603 ), 604 displayName = it.loadLabel(packageManager) as? String ?: "", 605 ) 606 } 607 608 return allowedProviders 609 } 610 611 /** 612 * Sends an update to the [_availableProviders] State flow. Collection info cache gets cleared 613 * because it is potentially stale. If the new set of available providers does not contain all 614 * of the previously available providers, then the UI should ideally clear itself immediately to 615 * avoid displaying any media items from a clud provider that is not currently available. To 616 * communicate this with the UI, [disruptiveDataUpdateChannel] might emit a Unit object. 617 * 618 * @param providers The list of new available providers. 619 */ 620 private suspend fun updateAvailableProviders(providers: List<Provider>) { 621 // Send refresh media request to Photo Picker. 622 // TODO(b/340246010): This is required even when there is no change in 623 // the [availableProviders] state flow because PhotoPicker relies on the 624 // UI to trigger a sync when the cloud provider changes. Further, a 625 // successful sync enables cloud queries, which then updates the UI. 626 refreshMedia(providers) 627 628 // refresh count for preGranted media. 629 refreshPreGrantedItemsCount() 630 631 config.value.preSelectedUris?.let { fetchMediaDataForUris(it) } 632 633 val previouslyAvailableProviders = _availableProviders.value 634 635 _availableProviders.update { providers } 636 637 // If the available providers are not a superset of previously available 638 // providers, this is a disruptive data update that should ideally 639 // reset the UI. 640 if (!providers.containsAll(previouslyAvailableProviders)) { 641 Log.d(DataService.TAG, "Sending a disruptive data update notification.") 642 disruptiveDataUpdateChannel.send(Unit) 643 } 644 645 // Clear collection info cache immediately and update the cache from 646 // data source in a child coroutine. 647 collectionInfoState.clear() 648 } 649 650 override fun refreshPreGrantedItemsCount() { 651 // value for _preGrantedMediaCount being null signifies that the count has not been fetched 652 // yet for this photopicker session. 653 // This should only be used in ACTION_USER_SELECT_IMAGES_FOR_APP mode since grants only 654 // exist for this mode. 655 if ( 656 _preGrantedMediaCount.value == null && 657 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(config.value.action) 658 ) { 659 _preGrantedMediaCount.update { 660 mediaProviderClient.fetchMediaGrantsCount( 661 activeContentResolver.value, 662 config.value.callingPackageUid ?: -1, 663 ) 664 } 665 } 666 } 667 668 override fun fetchMediaDataForUris(uris: List<Uri>) { 669 // value for _preSelectionMediaData being null signifies that the data has not been fetched 670 // yet for this photopicker session. 671 if (_preSelectionMediaData.value == null && uris.isNotEmpty()) { 672 // Pre-selection state is not accessible cross-profile, so any time the 673 // [activeUserProfile] is not the Process owner's profile, pre-selections should not be 674 // refreshed and any cached state should not be updated to the UI. 675 if ( 676 userStatus.value.activeUserProfile.handle.identifier == 677 processOwnerHandle.getIdentifier() 678 ) { 679 _preSelectionMediaData.update { 680 mediaProviderClient.fetchFilteredMedia( 681 MediaPageKey(), 682 MediaStore.getPickImagesMaxLimit(), 683 activeContentResolver.value, 684 _availableProviders.value, 685 config.value, 686 uris, 687 ) 688 } 689 } 690 } 691 } 692 693 /** 694 * Sends a refresh media notification to the data source. This signal tells the data source to 695 * refresh its cache. 696 * 697 * @param providers The list of currently available providers. 698 */ 699 private fun refreshMedia(availableProviders: List<Provider>) { 700 if (availableProviders.isNotEmpty()) { 701 mediaProviderClient.refreshMedia( 702 availableProviders, 703 activeContentResolver.value, 704 config.value, 705 ) 706 } else { 707 Log.w(DataService.TAG, "Cannot refresh media when there are no providers available") 708 } 709 } 710 711 /** 712 * Fetch available providers from the data source and return it. If the [CloudMediaFeature] is 713 * turned off, the available list of providers received from the data source will filter out all 714 * providers that serve [MediaSource.Remote] items. 715 */ 716 private fun fetchAvailableProviders(): List<Provider> { 717 var availableProviders = 718 mediaProviderClient.fetchAvailableProviders(activeContentResolver.value) 719 if (!featureManager.isFeatureEnabled(CloudMediaFeature::class.java)) { 720 availableProviders = availableProviders.filter { it.mediaSource != MediaSource.REMOTE } 721 Log.i( 722 DataService.TAG, 723 "Cloud media feature is not enabled, available providers are " + 724 "updated to $availableProviders", 725 ) 726 } 727 return availableProviders 728 } 729 } 730