• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 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.categorygrid
18 
19 import androidx.compose.foundation.layout.Column
20 import androidx.compose.foundation.layout.fillMaxSize
21 import androidx.compose.foundation.layout.fillMaxWidth
22 import androidx.compose.foundation.layout.padding
23 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
24 import androidx.compose.material.icons.Icons
25 import androidx.compose.material.icons.outlined.Image
26 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.LaunchedEffect
29 import androidx.compose.runtime.getValue
30 import androidx.compose.runtime.remember
31 import androidx.compose.runtime.rememberCoroutineScope
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.graphics.vector.ImageVector
34 import androidx.compose.ui.platform.LocalConfiguration
35 import androidx.compose.ui.res.stringResource
36 import androidx.compose.ui.unit.dp
37 import androidx.lifecycle.compose.collectAsStateWithLifecycle
38 import androidx.paging.LoadState
39 import androidx.paging.PagingData
40 import androidx.paging.compose.collectAsLazyPagingItems
41 import com.android.modules.utils.build.SdkLevel
42 import com.android.photopicker.R
43 import com.android.photopicker.core.components.EmptyState
44 import com.android.photopicker.core.components.MediaGridItem
45 import com.android.photopicker.core.components.mediaGrid
46 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
47 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
48 import com.android.photopicker.core.embedded.LocalEmbeddedState
49 import com.android.photopicker.core.events.Event
50 import com.android.photopicker.core.events.LocalEvents
51 import com.android.photopicker.core.events.Telemetry
52 import com.android.photopicker.core.features.FeatureToken
53 import com.android.photopicker.core.features.LocalFeatureManager
54 import com.android.photopicker.core.navigation.LocalNavController
55 import com.android.photopicker.core.navigation.PhotopickerDestinations
56 import com.android.photopicker.core.obtainViewModel
57 import com.android.photopicker.core.selection.LocalSelection
58 import com.android.photopicker.core.theme.LocalWindowSizeClass
59 import com.android.photopicker.data.model.Group
60 import com.android.photopicker.extensions.navigateToPreviewMedia
61 import com.android.photopicker.features.preview.PreviewFeature
62 import kotlinx.coroutines.flow.Flow
63 import kotlinx.coroutines.flow.StateFlow
64 import kotlinx.coroutines.launch
65 
66 /**
67  * Primary composable for drawing the Mediaset content grid on
68  * [PhotopickerDestinations.MEDIASET_CONTENT_GRID]
69  *
70  * @param viewModel - A viewModel override for the composable. Normally, this is fetched via hilt
71  *   from the backstack entry by using obtainViewModel()
72  * @param flow - stateflow holding the mediaset for which the media needs to be presented.
73  */
74 @Composable
75 fun MediaSetContentGrid(
76     flow: StateFlow<Group.MediaSet?>,
77     viewModel: CategoryGridViewModel = obtainViewModel(),
78 ) {
79     val mediasetState by flow.collectAsStateWithLifecycle(initialValue = null)
80     val mediaset = mediasetState
81     Column(modifier = Modifier.fillMaxSize()) {
82         when (mediaset) {
83             null -> {}
84             else -> {
85                 val mediasetItems = remember(mediaset) { viewModel.getMediaSetContent(mediaset) }
86                 MediasetContentGrid(mediasetItems = mediasetItems)
87             }
88         }
89     }
90 }
91 
92 /** Initialises all the states and media source required to load media for the input [mediaset]. */
93 @Composable
MediasetContentGridnull94 private fun MediasetContentGrid(
95     mediasetItems: Flow<PagingData<MediaGridItem>>,
96     viewModel: CategoryGridViewModel = obtainViewModel(),
97 ) {
98     val featureManager = LocalFeatureManager.current
99     val isPreviewEnabled = remember { featureManager.isFeatureEnabled(PreviewFeature::class.java) }
100     val navController = LocalNavController.current
101     val items = mediasetItems.collectAsLazyPagingItems()
102     // Collect the selection to notify the mediaGrid of selection changes.
103     val selection by LocalSelection.current.flow.collectAsStateWithLifecycle()
104     val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
105     val selectionLimitExceededMessage =
106         stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit)
107     // Use the expanded layout any time the Width is Medium or larger.
108     val isExpandedScreen: Boolean =
109         when (LocalWindowSizeClass.current.widthSizeClass) {
110             WindowWidthSizeClass.Medium -> true
111             WindowWidthSizeClass.Expanded -> true
112             else -> false
113         }
114     val state = rememberLazyGridState()
115     val isEmbedded =
116         LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
117     val host = LocalEmbeddedState.current?.host
118     val scope = rememberCoroutineScope()
119     val events = LocalEvents.current
120     val configuration = LocalPhotopickerConfiguration.current
121     // Container encapsulating the mediaset title followed by its content in the form of a
122     // grid, the content also includes date and month separators.
123     Column(modifier = Modifier.fillMaxSize()) {
124         val isEmptyAndNoMorePages =
125             items.itemCount == 0 &&
126                 items.loadState.source.append is LoadState.NotLoading &&
127                 items.loadState.source.append.endOfPaginationReached
128         when {
129             isEmptyAndNoMorePages -> {
130                 val localConfig = LocalConfiguration.current
131                 val emptyStatePadding =
132                     remember(localConfig) { (localConfig.screenHeightDp * .20).dp }
133                 val (title, body, icon) = getEmptyStateContentForMediaset()
134                 EmptyState(
135                     modifier =
136                         if (SdkLevel.isAtLeastU() && isEmbedded && host != null) {
137                             // In embedded no need to give extra top padding to make empty
138                             // state title and body clearly visible in collapse mode (small view)
139                             Modifier.fillMaxWidth()
140                         } else {
141                             // Provide 20% of screen height as empty space above
142                             Modifier.fillMaxWidth().padding(top = emptyStatePadding)
143                         },
144                     icon = icon,
145                     title = title,
146                     body = body,
147                 )
148                 LaunchedEffect(Unit) {
149                     events.dispatch(
150                         Event.LogPhotopickerUIEvent(
151                             FeatureToken.CATEGORY_GRID.token,
152                             configuration.sessionId,
153                             configuration.callingPackageUid ?: -1,
154                             Telemetry.UiEvent.UI_LOADED_EMPTY_STATE,
155                         )
156                     )
157                 }
158             }
159             else -> {
160                 mediaGrid(
161                     items = items,
162                     isExpandedScreen = isExpandedScreen,
163                     selection = selection,
164                     onItemClick = { item ->
165                         if (item is MediaGridItem.MediaItem) {
166                             viewModel.handleMediaSetItemSelection(
167                                 item.media,
168                                 selectionLimitExceededMessage,
169                             )
170                         }
171                     },
172                     onItemLongPress = { item ->
173                         // If the [PreviewFeature] is enabled, launch the preview route.
174                         if (isPreviewEnabled && item is MediaGridItem.MediaItem) {
175                             // Dispatch UI event to log long pressing the media item
176                             scope.launch {
177                                 events.dispatch(
178                                     Event.LogPhotopickerUIEvent(
179                                         FeatureToken.PREVIEW.token,
180                                         configuration.sessionId,
181                                         configuration.callingPackageUid ?: -1,
182                                         Telemetry.UiEvent.PICKER_LONG_SELECT_MEDIA_ITEM,
183                                     )
184                                 )
185                             }
186                             // Dispatch UI event to log entry into preview mode
187                             scope.launch {
188                                 events.dispatch(
189                                     Event.LogPhotopickerUIEvent(
190                                         FeatureToken.PREVIEW.token,
191                                         configuration.sessionId,
192                                         configuration.callingPackageUid ?: -1,
193                                         Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE,
194                                     )
195                                 )
196                             }
197                             navController.navigateToPreviewMedia(item.media)
198                         }
199                     },
200                     state = state,
201                 )
202                 LaunchedEffect(Unit) {
203                     // Dispatch UI event to log loading of meadia set contents
204                     events.dispatch(
205                         Event.LogPhotopickerUIEvent(
206                             FeatureToken.CATEGORY_GRID.token,
207                             configuration.sessionId,
208                             configuration.callingPackageUid ?: -1,
209                             Telemetry.UiEvent.UI_LOADED_MEDIA_SETS_CONTENTS,
210                         )
211                     )
212                 }
213             }
214         }
215     }
216 }
217 
218 /**
219  * Return a generic content for the empty state.
220  *
221  * @return a [Triple] that contains the [Title, Body, Icon] for the empty state.
222  */
223 @Composable
getEmptyStateContentForMediasetnull224 private fun getEmptyStateContentForMediaset(): Triple<String, String, ImageVector> {
225     return Triple(
226         stringResource(R.string.photopicker_photos_empty_state_title),
227         stringResource(R.string.photopicker_photos_empty_state_body),
228         Icons.Outlined.Image,
229     )
230 }
231