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.core.events
18
19 import android.media.ApplicationMediaCapabilities
20 import android.media.MediaFeature
21 import android.provider.MediaStore
22 import com.android.photopicker.core.configuration.PhotopickerConfiguration
23 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
24 import com.android.photopicker.core.features.FeatureManager
25 import com.android.photopicker.core.features.FeatureToken
26 import com.android.photopicker.core.navigation.PhotopickerDestinations
27 import com.android.photopicker.core.selection.Selection
28 import com.android.photopicker.core.theme.AccentColorHelper
29 import com.android.photopicker.core.user.UserMonitor
30 import com.android.photopicker.core.user.UserProfile
31 import com.android.photopicker.data.DataService
32 import com.android.photopicker.data.model.Media
33 import com.android.photopicker.data.model.MediaSource
34 import com.android.photopicker.extensions.getUserProfilesVisibleToPhotopicker
35 import com.android.photopicker.features.search.SearchFeature
36 import dagger.Lazy
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.flow.first
39 import kotlinx.coroutines.launch
40
41 /** Dispatches an event to log all details with which the photopicker launched */
42 fun dispatchReportPhotopickerApiInfoEvent(
43 coroutineScope: CoroutineScope,
44 lazyEvents: Lazy<Events>,
45 photopickerConfiguration: PhotopickerConfiguration,
46 pickerIntentAction: Telemetry.PickerIntentAction =
47 Telemetry.PickerIntentAction.UNSET_PICKER_INTENT_ACTION,
48 lazyFeatureManager: Lazy<FeatureManager>,
49 ) {
50 val dispatcherToken = FeatureToken.CORE.token
51 val sessionId = photopickerConfiguration.sessionId
52 // We always launch the picker in collapsed state. We track the state change as UI event.
53 val pickerSize = Telemetry.PickerSize.COLLAPSED
54 val mimeTypes = photopickerConfiguration.mimeTypes
55 val mediaFilter =
56 when {
57 mimeTypes.size > 1 &&
58 mimeTypes.any { it.startsWith("image/") } &&
59 mimeTypes.any { it.startsWith("video/") } -> Telemetry.MediaType.PHOTO_VIDEO
60 mimeTypes.size == 1 && mimeTypes.first().startsWith("image/") ->
61 Telemetry.MediaType.PHOTO
62 mimeTypes.size == 1 && mimeTypes.first().startsWith("video/") ->
63 Telemetry.MediaType.VIDEO
64 else -> Telemetry.MediaType.UNSET_MEDIA_TYPE
65 }
66 val maxPickedItemsCount = photopickerConfiguration.selectionLimit
67 val selectedTab =
68 when (photopickerConfiguration.startDestination) {
69 PhotopickerDestinations.PHOTO_GRID -> Telemetry.SelectedTab.PHOTOS
70 PhotopickerDestinations.ALBUM_GRID -> Telemetry.SelectedTab.ALBUMS
71 else -> Telemetry.SelectedTab.UNSET_SELECTED_TAB
72 }
73 val selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM
74 val isOrderedSelectionSet = photopickerConfiguration.pickImagesInOrder
75 // TODO(b/376822503): Creating a new instance of AccentColorHelper() to check color seems
76 // unnecessary. Fix later
77 val isAccentColorSet =
78 AccentColorHelper(inputColor = photopickerConfiguration.accentColor ?: -1)
79 .isValidAccentColorSet()
80 val isDefaultTabSet =
81 photopickerConfiguration.startDestination != PhotopickerDestinations.DEFAULT
82 val isCloudSearchEnabled = lazyFeatureManager.get().isFeatureEnabled(SearchFeature::class.java)
83 // TODO(b/376822503): Update when search is added
84 val isLocalSearchEnabled = false
85 val isTranscodingRequested: Boolean =
86 photopickerConfiguration.callingPackageMediaCapabilities != null
87 coroutineScope.launch {
88 lazyEvents
89 .get()
90 .dispatch(
91 Event.ReportPhotopickerApiInfo(
92 dispatcherToken = dispatcherToken,
93 sessionId = sessionId,
94 pickerIntentAction = pickerIntentAction,
95 pickerSize = pickerSize,
96 mediaFilter = mediaFilter,
97 maxPickedItemsCount = maxPickedItemsCount,
98 selectedTab = selectedTab,
99 selectedAlbum = selectedAlbum,
100 isOrderedSelectionSet = isOrderedSelectionSet,
101 isAccentColorSet = isAccentColorSet,
102 isDefaultTabSet = isDefaultTabSet,
103 isCloudSearchEnabled = isCloudSearchEnabled,
104 isLocalSearchEnabled = isLocalSearchEnabled,
105 isTranscodingRequested = isTranscodingRequested,
106 )
107 )
108 }
109 }
110
111 /** Dispatches an event to log App transcoding media capabilities if advertised by the app */
dispatchReportPickerAppMediaCapabilitiesnull112 fun dispatchReportPickerAppMediaCapabilities(
113 coroutineScope: CoroutineScope,
114 lazyEvents: Lazy<Events>,
115 photopickerConfiguration: PhotopickerConfiguration,
116 ) {
117 val dispatcherToken = FeatureToken.CORE.token
118 val sessionId = photopickerConfiguration.sessionId
119 val appMediaCapabilities: ApplicationMediaCapabilities? =
120 photopickerConfiguration.callingPackageMediaCapabilities
121 if (appMediaCapabilities != null) {
122 with(appMediaCapabilities) {
123 val supportedHdrTypes: IntArray = getEnumsForTypes(true, getSupportedHdrTypes())
124 val unsupportedHdrTypes: IntArray = getEnumsForTypes(false, getUnsupportedHdrTypes())
125 coroutineScope.launch {
126 lazyEvents
127 .get()
128 .dispatch(
129 Event.ReportPickerAppMediaCapabilities(
130 dispatcherToken = dispatcherToken,
131 sessionId = sessionId,
132 supportedHdrTypes = supportedHdrTypes,
133 unsupportedHdrTypes = unsupportedHdrTypes,
134 )
135 )
136 }
137 }
138 }
139 }
140
getEnumsForTypesnull141 private fun getEnumsForTypes(supported: Boolean, hdrTypesList: List<String>): IntArray {
142 var array: MutableList<Int> = mutableListOf()
143 for (type in hdrTypesList) {
144 when (type) {
145 MediaFeature.HdrType.DOLBY_VISION -> {
146 if (supported) array.add(Telemetry.HdrTypes.DOLBY_SUPPORTED.type)
147 else array.add(Telemetry.HdrTypes.DOLBY_UNSUPPORTED.type)
148 }
149 MediaFeature.HdrType.HDR10 -> {
150 if (supported) array.add(Telemetry.HdrTypes.HDR10_SUPPORTED.type)
151 else array.add(Telemetry.HdrTypes.HDR10_UNSUPPORTED.type)
152 }
153 MediaFeature.HdrType.HDR10_PLUS -> {
154 if (supported) array.add(Telemetry.HdrTypes.HDR10PLUS_SUPPORTED.type)
155 else array.add(Telemetry.HdrTypes.HDR10PLUS_UNSUPPORTED.type)
156 }
157 MediaFeature.HdrType.HLG -> {
158 if (supported) array.add(Telemetry.HdrTypes.HLG_SUPPORTED.type)
159 else array.add(Telemetry.HdrTypes.HLG_UNSUPPORTED.type)
160 }
161 }
162 }
163 return array.toIntArray()
164 }
165
166 /** Dispatches an event to log all the final state details of the picker */
dispatchReportPhotopickerSessionInfoEventnull167 fun dispatchReportPhotopickerSessionInfoEvent(
168 coroutineScope: CoroutineScope,
169 lazyEvents: Lazy<Events>,
170 photopickerConfiguration: PhotopickerConfiguration,
171 lazyDataService: Lazy<DataService>,
172 lazyUserMonitor: Lazy<UserMonitor>,
173 lazyMediaSelection: Lazy<Selection<Media>>,
174 pickerStatus: Telemetry.PickerStatus = Telemetry.PickerStatus.UNSET_PICKER_STATUS,
175 pickerCloseMethod: Telemetry.PickerCloseMethod =
176 Telemetry.PickerCloseMethod.UNSET_PICKER_CLOSE_METHOD,
177 ) {
178 val dispatcherToken = FeatureToken.CORE.token
179 val sessionId = photopickerConfiguration.sessionId
180 val packageUid = photopickerConfiguration.callingPackageUid ?: -1
181 val pickerSelection =
182 if (photopickerConfiguration.selectionLimit == 1) {
183 Telemetry.PickerSelection.SINGLE
184 } else {
185 Telemetry.PickerSelection.MULTIPLE
186 }
187 val cloudProviderUid =
188 lazyDataService
189 .get()
190 .availableProviders
191 .value
192 .firstOrNull { provider -> provider.mediaSource == MediaSource.REMOTE }
193 ?.uid ?: -1
194 val userProfile =
195 when (lazyUserMonitor.get().userStatus.value.activeUserProfile.profileType) {
196 UserProfile.ProfileType.PRIMARY -> Telemetry.UserProfile.PERSONAL
197 UserProfile.ProfileType.MANAGED -> Telemetry.UserProfile.WORK
198 else -> Telemetry.UserProfile.UNKNOWN
199 }
200 val pickedMediaItemsSet = lazyMediaSelection.get().flow.value
201 val pickedItemsCount = pickedMediaItemsSet.size
202 val pickedItemsSize = pickedMediaItemsSet.sumOf { it.sizeInBytes.toInt() }
203 val pickerMode =
204 when {
205 photopickerConfiguration.action == MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP ->
206 Telemetry.PickerMode.PERMISSION_MODE_PICKER
207 photopickerConfiguration.runtimeEnv == PhotopickerRuntimeEnv.ACTIVITY ->
208 Telemetry.PickerMode.REGULAR_PICKER
209 photopickerConfiguration.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED ->
210 Telemetry.PickerMode.EMBEDDED_PICKER
211 else -> Telemetry.PickerMode.UNSET_PICKER_MODE
212 }
213
214 coroutineScope.launch {
215 val profileSwitchButtonVisible =
216 lazyUserMonitor.get().userStatus.getUserProfilesVisibleToPhotopicker().first().size > 1
217 lazyEvents
218 .get()
219 .dispatch(
220 Event.ReportPhotopickerSessionInfo(
221 dispatcherToken = dispatcherToken,
222 sessionId = sessionId,
223 packageUid = packageUid,
224 pickerSelection = pickerSelection,
225 cloudProviderUid = cloudProviderUid,
226 userProfile = userProfile,
227 pickerStatus = pickerStatus,
228 pickedItemsCount = pickedItemsCount,
229 pickedItemsSize = pickedItemsSize,
230 profileSwitchButtonVisible = profileSwitchButtonVisible,
231 pickerMode = pickerMode,
232 pickerCloseMethod = pickerCloseMethod,
233 )
234 )
235 }
236 }
237
238 /** Dispatches an event to log media item status as selected / unselected */
dispatchReportPhotopickerMediaItemStatusEventnull239 fun dispatchReportPhotopickerMediaItemStatusEvent(
240 coroutineScope: CoroutineScope,
241 lazyEvents: Lazy<Events>,
242 photopickerConfiguration: PhotopickerConfiguration,
243 mediaItem: Media,
244 mediaStatus: Telemetry.MediaStatus,
245 pickerSize: Telemetry.PickerSize = Telemetry.PickerSize.UNSET_PICKER_SIZE,
246 ) {
247 val dispatcherToken = FeatureToken.CORE.token
248 val sessionId = photopickerConfiguration.sessionId
249 val selectionSource = mediaItem.selectionSource ?: Telemetry.MediaLocation.UNSET_MEDIA_LOCATION
250 val itemPosition = mediaItem.index ?: -1
251 val selectedAlbum = mediaItem.mediaItemAlbum
252 val mimeType = mediaItem.mimeType
253 // TODO(b/376822503): find live photo format
254 val mediaType =
255 if (mimeType.startsWith("image/")) {
256 if (mimeType.contains("gif")) {
257 Telemetry.MediaType.GIF
258 } else {
259 Telemetry.MediaType.PHOTO
260 }
261 } else if (mimeType.startsWith("video/")) {
262 Telemetry.MediaType.VIDEO
263 } else {
264 Telemetry.MediaType.OTHER
265 }
266 val cloudOnly = mediaItem.mediaSource == MediaSource.REMOTE
267 coroutineScope.launch {
268 lazyEvents
269 .get()
270 .dispatch(
271 Event.ReportPhotopickerMediaItemStatus(
272 dispatcherToken = dispatcherToken,
273 sessionId = sessionId,
274 mediaStatus = mediaStatus,
275 selectionSource = selectionSource,
276 itemPosition = itemPosition,
277 selectedAlbum = selectedAlbum,
278 mediaType = mediaType,
279 cloudOnly = cloudOnly,
280 pickerSize = pickerSize,
281 )
282 )
283 }
284 }
285
286 /**
287 * Dispatches an event to log the expansion state change event in picker. If [isExpanded], logs
288 * [Telemetry.UiEvent.EXPAND_PICKER], else logs [Telemetry.UiEvent.COLLAPSE_PICKER].
289 */
dispatchPhotopickerExpansionStateChangedEventnull290 fun dispatchPhotopickerExpansionStateChangedEvent(
291 coroutineScope: CoroutineScope,
292 lazyEvents: Lazy<Events>,
293 photopickerConfiguration: PhotopickerConfiguration,
294 isExpanded: Boolean,
295 ) {
296 val dispatcherToken = FeatureToken.CORE.token
297 val sessionId = photopickerConfiguration.sessionId
298 val packageUid = photopickerConfiguration.callingPackageUid ?: -1
299 val uiEvent =
300 if (isExpanded) Telemetry.UiEvent.EXPAND_PICKER else Telemetry.UiEvent.COLLAPSE_PICKER
301 coroutineScope.launch {
302 lazyEvents
303 .get()
304 .dispatch(
305 Event.LogPhotopickerUIEvent(
306 dispatcherToken = dispatcherToken,
307 sessionId = sessionId,
308 packageUid = packageUid,
309 uiEvent = uiEvent,
310 )
311 )
312 }
313 }
314