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