• 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.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