• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.photopicker.core.features
18 
19 import android.util.Log
20 import androidx.compose.runtime.Composable
21 import androidx.compose.ui.Modifier
22 import com.android.photopicker.core.configuration.PhotopickerConfiguration
23 import com.android.photopicker.core.events.Event
24 import com.android.photopicker.core.events.RegisteredEventClass
25 import com.android.photopicker.data.PrefetchDataService
26 import com.android.photopicker.features.albumgrid.AlbumGridFeature
27 import com.android.photopicker.features.browse.BrowseFeature
28 import com.android.photopicker.features.categorygrid.CategoryGridFeature
29 import com.android.photopicker.features.cloudmedia.CloudMediaFeature
30 import com.android.photopicker.features.navigationbar.NavigationBarFeature
31 import com.android.photopicker.features.overflowmenu.OverflowMenuFeature
32 import com.android.photopicker.features.photogrid.PhotoGridFeature
33 import com.android.photopicker.features.preparemedia.PrepareMediaFeature
34 import com.android.photopicker.features.preview.PreviewFeature
35 import com.android.photopicker.features.privacyexplainer.PrivacyExplainerFeature
36 import com.android.photopicker.features.profileselector.ProfileSelectorFeature
37 import com.android.photopicker.features.search.SearchFeature
38 import com.android.photopicker.features.selectionbar.SelectionBarFeature
39 import com.android.photopicker.features.snackbar.SnackbarFeature
40 import com.android.photopicker.util.mapOfDeferredWithTimeout
41 import java.util.concurrent.CopyOnWriteArraySet
42 import kotlinx.coroutines.CoroutineDispatcher
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.Deferred
45 import kotlinx.coroutines.Dispatchers
46 import kotlinx.coroutines.flow.StateFlow
47 import kotlinx.coroutines.flow.drop
48 import kotlinx.coroutines.launch
49 import kotlinx.coroutines.runBlocking
50 
51 /**
52  * The core class in the feature framework, the FeatureManager manages the registration,
53  * initialiation and compose calls for the compose UI.
54  *
55  * The feature manager is responsible for calling Features via the [PhotopickerFeature] interface
56  * framework, for various lifecycles, as well as providing the APIs for callers to inspect feature
57  * state, change configuration, and generate composable units for various UI [Location]s.
58  *
59  * @property configuration a collectable [StateFlow] of configuration changes
60  * @property scope A CoroutineScope that PhotopickerConfiguration updates are collected in.
61  * @property registeredFeatures A set of Registrations that correspond to (potentially) enabled
62  *   features.
63  */
64 class FeatureManager(
65     private val configuration: StateFlow<PhotopickerConfiguration>,
66     private val scope: CoroutineScope,
67     private val prefetchDataService: PrefetchDataService,
68     // This is in the constructor to allow tests to swap in test features.
69     private val registeredFeatures: Set<FeatureRegistration> =
70         FeatureManager.KNOWN_FEATURE_REGISTRATIONS,
71     // These are in the constructor to allow tests to swap in core event overrides.
72     private val coreEventsConsumed: Set<RegisteredEventClass> = FeatureManager.CORE_EVENTS_CONSUMED,
73     private val coreEventsProduced: Set<RegisteredEventClass> = FeatureManager.CORE_EVENTS_PRODUCED,
74     private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
75 ) {
76     companion object {
77         val TAG: String = "PhotopickerFeatureManager"
78 
79         /*
80          * The list of known [FeatureRegistration]s.
81          * Any features that include their registration here, are subject to be enabled by the
82          * [FeatureManager] when their [FeatureRegistration#isEnabled] returns true.
83          */
84         val KNOWN_FEATURE_REGISTRATIONS: Set<FeatureRegistration> =
85             setOf(
86                 PhotoGridFeature.Registration,
87                 SelectionBarFeature.Registration,
88                 NavigationBarFeature.Registration,
89                 PreviewFeature.Registration,
90                 ProfileSelectorFeature.Registration,
91                 AlbumGridFeature.Registration,
92                 SnackbarFeature.Registration,
93                 CloudMediaFeature.Registration,
94                 OverflowMenuFeature.Registration,
95                 PrivacyExplainerFeature.Registration,
96                 BrowseFeature.Registration,
97                 SearchFeature.Registration,
98                 PrepareMediaFeature.Registration,
99                 CategoryGridFeature.Registration,
100             )
101 
102         /* The list of events that the core library consumes. */
103         val CORE_EVENTS_CONSUMED: Set<RegisteredEventClass> = setOf()
104 
105         /* The list of events that the core library produces. */
106         val CORE_EVENTS_PRODUCED: Set<RegisteredEventClass> =
107             setOf(
108                 Event.ShowSnackbarMessage::class.java,
109                 Event.ReportPhotopickerSessionInfo::class.java,
110                 Event.ReportPhotopickerApiInfo::class.java,
111                 Event.LogPhotopickerUIEvent::class.java,
112                 Event.LogPhotopickerAlbumOpenedUIEvent::class.java,
113                 Event.ReportPhotopickerMediaItemStatus::class.java,
114                 Event.LogPhotopickerPreviewInfo::class.java,
115                 Event.LogPhotopickerMenuInteraction::class.java,
116                 Event.LogPhotopickerBannerInteraction::class.java,
117                 Event.LogPhotopickerMediaLibraryInfo::class.java,
118                 Event.LogPhotopickerPageInfo::class.java,
119                 Event.ReportPhotopickerMediaGridSyncInfo::class.java,
120                 Event.ReportPhotopickerAlbumSyncInfo::class.java,
121                 Event.ReportPhotopickerSearchInfo::class.java,
122                 Event.ReportSearchDataExtractionDetails::class.java,
123                 Event.ReportEmbeddedPhotopickerInfo::class.java,
124                 Event.ReportPickerAppMediaCapabilities::class.java,
125                 Event.ReportTranscodingVideoDetails::class.java,
126             )
127     }
128 
129     // The internal mutable set of enabled features.
130     // This field is read in the public method [isFeatureEnabled] which can be called from both the
131     // main thread as well as various background threads, so ensure concurrency by using a slower,
132     // but thread safe data structure. This list is fairly small (roughly the size of
133     // KNOWN_FEATURE_REGISTRATIONS), and it's access is retrieval heavy rather than mutation heavy.
134     private val _enabledFeatures: CopyOnWriteArraySet<PhotopickerFeature> = CopyOnWriteArraySet()
135 
136     // The internal map of claimed [FeatureToken] to the claiming [PhotopickerFeature]
137     private val _tokenMap: HashMap<String, PhotopickerFeature> = HashMap()
138 
139     // A map containing the deferred results of all prefetched data.
140     // Prefetched data is the data that features can request FeatureManager to fetch for them
141     // (typically from a different process), before the features have to decide if they are enabled
142     // or not.
143     private val _deferredPrefetchResults: Map<PrefetchResultKey, Deferred<Any?>> =
144         getDeferredPrefetchResults()
145 
146     /* Returns an immutable copy rather than the actual set. */
147     val enabledFeatures: Set<PhotopickerFeature>
148         get() = _enabledFeatures.toSet()
149 
150     val enabledUiFeatures: Set<PhotopickerUiFeature>
151         get() = _enabledFeatures.filterIsInstance<PhotopickerUiFeature>().toSet()
152 
153     /*
154      * The location registry for [PhotopickerUiFeature].
155      *
156      * The key in this map is the UI [Location]
157      * The value is a *always* a sorted "priority-descending" set of Pairs
158      *
159      * Each pair represents a Feature which would like to draw UI at this Location, and the Priority
160      * with which it would like to do so.
161      *
162      * It is critical that the list always remains sorted to avoid drawing the wrong element for a
163      * Location with a limited number of slots. It can be sorted with [PriorityDescendingComparator]
164      * to keep features sorted in order of Priority, then Registration (insertion) order.
165      *
166      * For Features who set the default Location [Priority.REGISTRATION_ORDER] they will
167      * be drawn in order of registration in the [FeatureManager.KNOWN_FEATURE_REGISTRATIONS].
168      *
169      */
170     private val locationRegistry: HashMap<Location, MutableList<Pair<PhotopickerUiFeature, Int>>> =
171         HashMap()
172 
173     /* Instantiate a shared single instance of our custom priority sorter to save memory */
174     private val priorityDescending: Comparator<Pair<Any, Int>> = PriorityDescendingComparator()
175 
176     init {
177         initializeFeatureSet()
178 
179         // Begin collecting the PhotopickerConfiguration and update the feature configuration
180         // accordingly.
<lambda>null181         scope.launch {
182             // Drop the first value here to prevent initializing twice.
183             // (initializeFeatureSet will pick up the first value on its own.)
184             configuration.drop(1).collect { onConfigurationChanged(it) }
185         }
186     }
187 
188     /**
189      * Set a new configuration value in the FeatureManager.
190      *
191      * Warning: This is an expensive operation, and should be batched if multiple configuration
192      * updates are expected in the near future.
193      * 1. Notify all existing features of the pending configuration change,
194      * 2. Wipe existing features
195      * 3. Re-initialize Feature set with new configuration
196      */
onConfigurationChangednull197     private fun onConfigurationChanged(newConfig: PhotopickerConfiguration) {
198         Log.d(TAG, """Configuration has changed, re-initializing. $newConfig""")
199 
200         // Notify all active features of the incoming config change.
201         _enabledFeatures.forEach { it.onConfigurationChanged(newConfig) }
202 
203         // Drop all registrations and prepare to reinitialize.
204         resetAllRegistrations()
205 
206         // Re-initialize.
207         initializeFeatureSet(newConfig)
208     }
209 
210     /** Drops all known registrations and returns to a pre-initialization state */
resetAllRegistrationsnull211     private fun resetAllRegistrations() {
212         _enabledFeatures.clear()
213         _tokenMap.clear()
214         locationRegistry.clear()
215     }
216 
217     /**
218      * 1. Collects all prefetch data requests from features.
219      * 2. Tries to fetch all prefetched results asynchronously (in parallel on background threads).
220      *    Each prefetch result should be received within a timeout of 200ms, otherwise the task will
221      *    be cancelled and the result will be null. If an error occurs, it will be swallowed and the
222      *    result will be null.
223      *
224      * @return A Map of prefetch result key to a Deferred prefetch result value.
225      */
getDeferredPrefetchResultsnull226     private fun getDeferredPrefetchResults(): Map<PrefetchResultKey, Deferred<Any?>> {
227         Log.d(TAG, "Beginning prefetching results in the background.")
228 
229         val prefetchRequestMap:
230             MutableMap<PrefetchResultKey, suspend (PrefetchDataService) -> Any?> =
231             mutableMapOf()
232         registeredFeatures
233             .mapNotNull { it.getPrefetchRequest(configuration.value) }
234             .forEach { prefetchRequestMap.putAll(it) }
235 
236         val prefetchDeferredResultsMap =
237             runBlocking(dispatcher) {
238                 mapOfDeferredWithTimeout<PrefetchResultKey, PrefetchDataService>(
239                     inputMap = prefetchRequestMap,
240                     input = prefetchDataService,
241                     timeoutMillis = 250L,
242                     backgroundScope = scope,
243                     dispatcher = dispatcher,
244                 )
245             }
246 
247         Log.d(
248             TAG,
249             "Creation of deferred prefetch results map is complete for keys: " +
250                 "${prefetchRequestMap.keys}",
251         )
252 
253         return prefetchDeferredResultsMap
254     }
255 
256     /**
257      * For the provided set of [FeatureRegistration]s, attempt to initialize the runtime Feature set
258      * with the current [PhotopickerConfiguration].
259      *
260      * @param config The configuration to use for initialization. Defaults to the current
261      *   configuration.
262      * @throws [IllegalStateException] if multiple features attempt to claim the same
263      *   [FeatureToken].
264      */
initializeFeatureSetnull265     private fun initializeFeatureSet(config: PhotopickerConfiguration = configuration.value) {
266         Log.d(TAG, "Beginning feature initialization with config: ${configuration.value}")
267 
268         for (featureCompanion in registeredFeatures) {
269             if (featureCompanion.isEnabled(config, _deferredPrefetchResults)) {
270                 val feature = featureCompanion.build(this)
271                 _enabledFeatures.add(feature)
272                 if (_tokenMap.contains(feature.token))
273                     throw IllegalStateException(
274                         "A feature has already claimed ${feature.token}. " +
275                             "Tokens must be unique for any given configuration."
276                     )
277                 _tokenMap.put(feature.token, feature)
278                 if (feature is PhotopickerUiFeature) registerLocationsForFeature(feature)
279             }
280         }
281 
282         validateEventRegistrations()
283 
284         Log.d(
285             TAG,
286             "Feature initialization complete. Features: ${_enabledFeatures.map { it.token }}",
287         )
288     }
289 
290     /**
291      * Inspect the event registrations for consumed and produced events based on the core library
292      * and the current set of enabledFeatures.
293      *
294      * This check ensures that all events that need to be consumed have at least one possible
295      * producer (it does not guarantee the event will actually be produced).
296      *
297      * In the event consumed events are not produced, this behaves differently depending on the
298      * [PhotopickerConfiguration].
299      * - If [PhotopickerConfiguration.deviceIsDebuggable] this will throw [IllegalStateException]
300      *   This is done to try to prevent bad configurations from escaping test and dev builds.
301      * - Else This will Log a warning, but allow initialization to proceed to avoid a runtime crash.
302      */
validateEventRegistrationsnull303     private fun validateEventRegistrations() {
304         // Include the events the CORE library expects to consume in the list of consumed events,
305         // along with all enabledFeatures.
306         val consumedEvents: Set<RegisteredEventClass> =
307             listOf(coreEventsConsumed, *_enabledFeatures.map { it.eventsConsumed }.toTypedArray())
308                 .flatten()
309                 .toSet()
310 
311         // Include the events the CORE library expects to produce in the list of produced events,
312         // along with all enabledFeatures.
313         val producedEvents: Set<RegisteredEventClass> =
314             listOf(coreEventsProduced, *_enabledFeatures.map { it.eventsProduced }.toTypedArray())
315                 .flatten()
316                 .toSet()
317 
318         val consumedButNotProduced = (consumedEvents subtract producedEvents)
319 
320         if (consumedButNotProduced.isNotEmpty()) {
321             if (configuration.value.deviceIsDebuggable) {
322                 // If the device is a debuggable build, throw an [IllegalStateException] to ensure
323                 // that unregistered events don't introduce un-intentional side-effects.
324                 throw IllegalStateException(
325                     "Events are expected to be consumed that are not produced: " +
326                         "$consumedButNotProduced"
327                 )
328             } else {
329                 // If this is a production build, this is still a bad state, but avoid crashing, and
330                 // put a note in the logs that the event registration is potentially problematic.
331                 Log.w(
332                     TAG,
333                     "Events are expected to be consumed that are not produced: " +
334                         "$consumedButNotProduced",
335                 )
336             }
337         }
338     }
339 
340     /**
341      * Adds the [PhotopickerUiFeature]'s registered locations to the internal location registry.
342      *
343      * To minimize memory footprint, the location is only initialized if at least one feature has it
344      * in its list of registeredLocations. This avoids the underlying registry carrying empty lists
345      * for location that no feature wishes to use.
346      *
347      * The list that is initialized uses the local [PriorityDescendingComparator] to keep the
348      * features at that location sorted by priority.
349      */
registerLocationsForFeaturenull350     private fun registerLocationsForFeature(feature: PhotopickerUiFeature) {
351         val locationPairs = feature.registerLocations()
352 
353         for ((first, second) in locationPairs) {
354             // Try to add the feature to this location's registry.
355             locationRegistry.get(first)?.let {
356                 it.add(Pair(feature, second))
357                 it.sortWith(priorityDescending)
358             }
359                 // If this is the first registration for this location, initialize the list and add
360                 // the current feature to the registry for this location.
361                 ?: locationRegistry.put(first, mutableListOf(Pair(feature, second)))
362         }
363     }
364 
365     /**
366      * Whether or not a requested feature is enabled
367      *
368      * @param featureClass - The class of the feature (doesn't require an instance to be created)
369      * @return true if the requested feature is enabled in the current session.
370      */
isFeatureEnablednull371     fun isFeatureEnabled(featureClass: Class<out PhotopickerFeature>): Boolean {
372         return _enabledFeatures.any { it::class.java == featureClass }
373     }
374 
375     /**
376      * Check if a provided event can be dispatched with the current enabled feature set.
377      *
378      * This is called when an event is dispatched to ensure that features cannot dispatch events
379      * that they do not include in their [PhotopickerFeature.eventsProduced] event registry.
380      *
381      * This checks the claiming [dispatcherToken] in the Event and checks the corresponding
382      * feature's event registry to ensure the event has claimed it dispatches the particular Event
383      * class. In the event of a CORE library event, check the internal mapping owned by
384      * [FeatureManager].
385      *
386      * @return Whether the event complies with the event registry.
387      */
isEventDispatchablenull388     fun isEventDispatchable(event: Event): Boolean {
389         if (event.dispatcherToken == FeatureToken.CORE.token)
390             return coreEventsProduced.contains(event::class.java)
391         return _tokenMap.get(event.dispatcherToken)?.eventsProduced?.contains(event::class.java)
392             ?: false
393     }
394 
395     /**
396      * Checks the run-time (current) maximum size (in terms of number of children created) of the
397      * provided [Location] in the [FeatureManager] internal [locationRegistry].
398      *
399      * This allows features to determine if a given [composeLocation] call will actually create any
400      * child elements at the location.
401      *
402      * The size returned is always stable for the current [PhotopickerConfiguration] but may change
403      * if the configuration is changed, since features could be added or removed under the new
404      * configuration.
405      *
406      * NOTE: This only returns the number of children, there is no way to directly interact with the
407      * feature classes registered at the given location.
408      *
409      * @param location The location to check the size of.
410      * @return the max number of children of the location. Cannot be negative.
411      * @see [composeLocation] for rendering the children of a [Location] in the compose tree.
412      */
getSizeOfLocationInRegistrynull413     fun getSizeOfLocationInRegistry(location: Location): Int {
414         // There is no guarantee the [Location] exists in the registry, since it is initialized
415         // lazily, its possible that features have not been registered for the current
416         // configuration.
417         return locationRegistry.get(location)?.size ?: 0
418     }
419 
420     /**
421      * Calls all of the relevant compose methods for all enabled [PhotopickerUiFeature] that have
422      * the [Location] in their registered locations, in their declared priority descending order.
423      *
424      * Features with a higher priority are composed first.
425      *
426      * This is the primary API for features to compose UI using the [Location] framework.
427      *
428      * This can result in an empty [Composable] if no features have the provided [Location] in their
429      * list of registered locations.
430      *
431      * Additional parameters can be passed via the [LocationParams] interface for providing
432      * functionality such as click handlers or passing primitive data.
433      *
434      * @param location The UI location that needs to be composed
435      * @param maxSlots (Optional, default unlimited) The maximum number of features that can compose
436      *   at this location. If set, this will call features in priority order until all slots of been
437      *   exhausted.
438      * @param modifier (Optional) A [Modifier] to pass in the compose call.
439      * @param params (Optional) A [LocationParams] to pass in the compose call.
440      * @see [LocationParams]
441      *
442      * Note: Be careful where this is called in the UI tree. Calling this inside of a composable
443      * that is regularly re-composed will result in the entire sub tree being re-composed, which can
444      * impact performance.
445      */
446     @Composable
composeLocationnull447     fun composeLocation(
448         location: Location,
449         maxSlots: Int? = null,
450         modifier: Modifier = Modifier,
451         params: LocationParams = LocationParams.None,
452     ) {
453         val featurePairs = locationRegistry.get(location)
454 
455         // There is no guarantee the [Location] exists in the registry, since it is initialized
456         // lazily, its possible that features have not been registered.
457         featurePairs?.let {
458             for (feature in featurePairs.take(maxSlots ?: featurePairs.size)) {
459                 Log.d(TAG, "Composing for $location for $feature")
460                 feature.first.compose(location, modifier, params)
461             }
462         }
463     }
464 }
465