• 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.features.cloudmedia
18 
19 import android.content.Intent
20 import android.provider.MediaStore
21 import androidx.compose.runtime.Composable
22 import androidx.compose.runtime.rememberCoroutineScope
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.platform.LocalContext
25 import androidx.compose.ui.res.stringResource
26 import com.android.photopicker.R
27 import com.android.photopicker.core.banners.Banner
28 import com.android.photopicker.core.banners.BannerDefinitions
29 import com.android.photopicker.core.banners.BannerState
30 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
31 import com.android.photopicker.core.configuration.PhotopickerConfiguration
32 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
33 import com.android.photopicker.core.events.Event
34 import com.android.photopicker.core.events.LocalEvents
35 import com.android.photopicker.core.events.RegisteredEventClass
36 import com.android.photopicker.core.events.Telemetry
37 import com.android.photopicker.core.features.FeatureManager
38 import com.android.photopicker.core.features.FeatureRegistration
39 import com.android.photopicker.core.features.FeatureToken
40 import com.android.photopicker.core.features.Location
41 import com.android.photopicker.core.features.LocationParams
42 import com.android.photopicker.core.features.PhotopickerUiFeature
43 import com.android.photopicker.core.features.PrefetchResultKey
44 import com.android.photopicker.core.features.Priority
45 import com.android.photopicker.core.navigation.Route
46 import com.android.photopicker.core.user.UserMonitor
47 import com.android.photopicker.data.DataService
48 import com.android.photopicker.data.model.CollectionInfo
49 import com.android.photopicker.data.model.MediaSource
50 import com.android.photopicker.data.model.Provider
51 import com.android.photopicker.features.overflowmenu.OverflowMenuItem
52 import kotlinx.coroutines.Deferred
53 import kotlinx.coroutines.launch
54 
55 /**
56  * Feature class for the Photopicker's cloud media implementation.
57  *
58  * This feature adds the Cloud media preloader for preloading off-device content before the
59  * Photopicker session ends.
60  */
61 class CloudMediaFeature : PhotopickerUiFeature {
62     companion object Registration : FeatureRegistration {
63         override val TAG: String = "PhotopickerCloudMediaFeature"
64 
isEnablednull65         override fun isEnabled(
66             config: PhotopickerConfiguration,
67             deferredPrefetchResultsMap: Map<PrefetchResultKey, Deferred<Any?>>,
68         ): Boolean {
69 
70             // Cloud media is not available in permission mode.
71             if (config.action == MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) return false
72 
73             return config.flags.CLOUD_MEDIA_ENABLED &&
74                 config.flags.CLOUD_ALLOWED_PROVIDERS.isNotEmpty()
75         }
76 
buildnull77         override fun build(featureManager: FeatureManager) = CloudMediaFeature()
78     }
79 
80     override val token = FeatureToken.CLOUD_MEDIA.token
81 
82     override val ownedBanners: Set<BannerDefinitions> =
83         setOf(
84             BannerDefinitions.CLOUD_CHOOSE_ACCOUNT,
85             BannerDefinitions.CLOUD_CHOOSE_PROVIDER,
86             BannerDefinitions.CLOUD_MEDIA_AVAILABLE,
87         )
88 
89     override suspend fun getBannerPriority(
90         banner: BannerDefinitions,
91         bannerState: BannerState?,
92         config: PhotopickerConfiguration,
93         dataService: DataService,
94         userMonitor: UserMonitor,
95     ): Int {
96 
97         val isEmbedded = config.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
98         // If any of the banners owned by [CloudMediaFeature] have been previously dismissed, then
99         // return a disabled priority.
100         if (bannerState?.dismissed == true) {
101             return Priority.DISABLED.priority
102         }
103 
104         // Attempt to find a [REMOTE] provider in the available list of providers.
105         val currentCloudProvider: Provider? =
106             dataService.availableProviders.value.firstOrNull {
107                 it.mediaSource == MediaSource.REMOTE
108             }
109 
110         // If one is found, fetch the collectionInfo for that provider.
111         val collectionInfo: CollectionInfo? =
112             currentCloudProvider?.let { dataService.getCollectionInfo(it) }
113 
114         return when (banner) {
115             BannerDefinitions.CLOUD_CHOOSE_PROVIDER -> {
116                 return when {
117                     // Don't show in Embedded, as the banner starts an activity which can cause a
118                     // crash.
119                     isEmbedded -> Priority.DISABLED.priority
120 
121                     // If there is no current provider, but a list of allowed providers exists
122                     currentCloudProvider == null &&
123                         dataService.getAllAllowedProviders().isNotEmpty() ->
124                         Priority.MEDIUM.priority
125 
126                     // There's a cloud provider set, so don't show
127                     else -> Priority.DISABLED.priority
128                 }
129             }
130             BannerDefinitions.CLOUD_CHOOSE_ACCOUNT -> {
131                 collectionInfo?.let {
132                     when {
133                         // Don't show in Embedded, as the banner starts an activity which can cause
134                         // a crash.
135                         isEmbedded -> Priority.DISABLED.priority
136 
137                         // If there is no current cloud provider account
138                         it.accountName == null -> Priority.MEDIUM.priority
139 
140                         // There's a cloud provider account set, so don't show
141                         else -> Priority.DISABLED.priority
142                     }
143                 } ?: Priority.DISABLED.priority
144             }
145             BannerDefinitions.CLOUD_MEDIA_AVAILABLE -> {
146                 collectionInfo?.let {
147                     if (it.accountName != null && it.collectionId != null) {
148                         Priority.MEDIUM.priority
149                     } else {
150                         Priority.DISABLED.priority
151                     }
152                 } ?: Priority.DISABLED.priority
153             }
154             else ->
155                 throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
156         }
157     }
158 
buildBannernull159     override suspend fun buildBanner(
160         banner: BannerDefinitions,
161         dataService: DataService,
162         userMonitor: UserMonitor,
163     ): Banner {
164 
165         val cloudProvider: Provider? =
166             dataService.availableProviders.value.firstOrNull {
167                 it.mediaSource == MediaSource.REMOTE
168             }
169 
170         val collectionInfo: CollectionInfo? =
171             cloudProvider?.let { dataService.getCollectionInfo(it) }
172 
173         return when (banner) {
174             BannerDefinitions.CLOUD_CHOOSE_PROVIDER -> cloudChooseProviderBanner
175             BannerDefinitions.CLOUD_CHOOSE_ACCOUNT ->
176                 buildCloudChooseAccountBanner(
177                     cloudProvider =
178                         checkNotNull(cloudProvider) { "cloudProvider was null during buildBanner" },
179                     collectionInfo =
180                         checkNotNull(collectionInfo) {
181                             "collectionInfo was null during buildBanner"
182                         },
183                 )
184             BannerDefinitions.CLOUD_MEDIA_AVAILABLE ->
185                 buildCloudMediaAvailableBanner(
186                     cloudProvider =
187                         checkNotNull(cloudProvider) { "cloudProvider was null during buildBanner" },
188                     collectionInfo =
189                         checkNotNull(collectionInfo) {
190                             "collectionInfo was null during buildBanner"
191                         },
192                 )
193             else ->
194                 throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
195         }
196     }
197 
198     /** Events consumed by Cloud Media */
199     override val eventsConsumed = setOf<RegisteredEventClass>()
200 
201     /** Events produced by the Cloud Media */
202     override val eventsProduced =
203         setOf<RegisteredEventClass>(
204             Event.LogPhotopickerMenuInteraction::class.java,
205             Event.LogPhotopickerUIEvent::class.java,
206         )
207 
registerLocationsnull208     override fun registerLocations(): List<Pair<Location, Int>> {
209         return listOf(
210             // Medium priority for OVERFLOW_MENU_ITEMS so that [BrowseFeature] can
211             // have the top spot if it's enabled.
212             Pair(Location.OVERFLOW_MENU_ITEMS, Priority.MEDIUM.priority)
213         )
214     }
215 
registerNavigationRoutesnull216     override fun registerNavigationRoutes(): Set<Route> {
217         return setOf()
218     }
219 
220     @Composable
composenull221     override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
222         val events = LocalEvents.current
223         val scope = rememberCoroutineScope()
224         val configuration = LocalPhotopickerConfiguration.current
225         when (location) {
226             Location.OVERFLOW_MENU_ITEMS -> {
227                 val context = LocalContext.current
228                 val clickAction = params as? LocationParams.WithClickAction
229                 OverflowMenuItem(
230                     label = stringResource(R.string.photopicker_overflow_cloud_media_app),
231                     onClick = {
232                         clickAction?.onClick()
233                         context.startActivity(Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS))
234                         // Dispatch event to log user's interactiuon with the cloud settings menu
235                         // item in the photopicker
236                         scope.launch {
237                             events.dispatch(
238                                 Event.LogPhotopickerMenuInteraction(
239                                     token,
240                                     configuration.sessionId,
241                                     configuration.callingPackageUid ?: -1,
242                                     Telemetry.MenuItemSelected.CLOUD_SETTINGS,
243                                 )
244                             )
245                         }
246                     },
247                 )
248             }
249             else -> {}
250         }
251     }
252 }
253