• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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