• 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.PaddingValues
21 import androidx.compose.foundation.layout.fillMaxSize
22 import androidx.compose.foundation.layout.fillMaxWidth
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.lazy.grid.GridCells
25 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
26 import androidx.compose.material.icons.Icons
27 import androidx.compose.material.icons.outlined.Group
28 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.LaunchedEffect
31 import androidx.compose.runtime.getValue
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.rememberCoroutineScope
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.graphics.vector.ImageVector
36 import androidx.compose.ui.platform.LocalConfiguration
37 import androidx.compose.ui.res.stringResource
38 import androidx.compose.ui.unit.dp
39 import androidx.lifecycle.compose.collectAsStateWithLifecycle
40 import androidx.paging.LoadState
41 import androidx.paging.compose.collectAsLazyPagingItems
42 import com.android.modules.utils.build.SdkLevel
43 import com.android.photopicker.R
44 import com.android.photopicker.core.components.EmptyState
45 import com.android.photopicker.core.components.MediaGridItem
46 import com.android.photopicker.core.components.mediaGrid
47 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
48 import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
49 import com.android.photopicker.core.embedded.LocalEmbeddedState
50 import com.android.photopicker.core.events.Event
51 import com.android.photopicker.core.events.LocalEvents
52 import com.android.photopicker.core.events.Telemetry
53 import com.android.photopicker.core.features.FeatureToken
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.theme.LocalWindowSizeClass
58 import com.android.photopicker.data.model.CategoryType
59 import com.android.photopicker.data.model.Group
60 import com.android.photopicker.extensions.navigateToMediaSetContentGrid
61 import kotlinx.coroutines.flow.StateFlow
62 import kotlinx.coroutines.launch
63 
64 /** The number of grid cells per row for Phone / narrow layouts */
65 private val CELLS_PER_ROW_FOR_MEDIASET_GRID = 3
66 /** The number of grid cells per row for Tablet / expanded layouts */
67 private val CELLS_PER_ROW_EXPANDED_FOR_MEDIASET_GRID = 4
68 /** The amount of padding to use around each cell in the mediaset grid. */
69 private val MEASUREMENT_HORIZONTAL_CELL_SPACING_MEDIASET_GRID = 1.dp
70 
71 /**
72  * Primary composable for drawing the main MediasetGrid on [PhotopickerDestinations.MEDIASET_GRID]
73  *
74  * @param viewModel - A viewModel override for the composable. Normally, this is fetched via hilt
75  *   from the backstack entry by using obtainViewModel()
76  */
77 @Composable
78 fun MediaSetGrid(
79     flow: StateFlow<Group.Category?>,
80     viewModel: CategoryGridViewModel = obtainViewModel(),
81 ) {
82     val categoryState by flow.collectAsStateWithLifecycle(initialValue = null)
83     val category = categoryState
84     when (category) {
85         null -> {}
86         else -> {
87             val items = remember(category) { viewModel.getMediaSets(category) }
88             val state = rememberLazyGridState()
89             val navController = LocalNavController.current
90             val scope = rememberCoroutineScope()
91             val events = LocalEvents.current
92             val configuration = LocalPhotopickerConfiguration.current
93 
94             val isEmbedded =
95                 LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
96             val host = LocalEmbeddedState.current?.host
97             // Use the expanded layout any time the Width is Medium or larger.
98             val isExpandedScreen: Boolean =
99                 when (LocalWindowSizeClass.current.widthSizeClass) {
100                     WindowWidthSizeClass.Medium -> true
101                     WindowWidthSizeClass.Expanded -> true
102                     else -> false
103                 }
104             val mediaSetItems = items.collectAsLazyPagingItems()
105             Column(modifier = Modifier.fillMaxSize()) {
106                 val isEmptyAndNoMorePages =
107                     mediaSetItems.itemCount == 0 &&
108                         mediaSetItems.loadState.source.append is LoadState.NotLoading &&
109                         mediaSetItems.loadState.source.append.endOfPaginationReached
110                 when {
111                     isEmptyAndNoMorePages -> {
112                         val localConfig = LocalConfiguration.current
113                         val emptyStatePadding =
114                             remember(localConfig) { (localConfig.screenHeightDp * .20).dp }
115                         val (title, body, icon) =
116                             getEmptyStateContentForMediaset(category.categoryType)
117                         EmptyState(
118                             modifier =
119                                 if (SdkLevel.isAtLeastU() && isEmbedded && host != null) {
120                                     // In embedded no need to give extra top padding to make empty
121                                     // state title and body clearly visible in collapse mode (small
122                                     // view)
123                                     Modifier.fillMaxWidth()
124                                 } else {
125                                     // Provide 20% of screen height as empty space above
126                                     Modifier.fillMaxWidth().padding(top = emptyStatePadding)
127                                 },
128                             icon = icon,
129                             title = title,
130                             body = body,
131                         )
132                         LaunchedEffect(Unit) {
133                             events.dispatch(
134                                 Event.LogPhotopickerUIEvent(
135                                     FeatureToken.CATEGORY_GRID.token,
136                                     configuration.sessionId,
137                                     configuration.callingPackageUid ?: -1,
138                                     Telemetry.UiEvent.UI_LOADED_EMPTY_STATE,
139                                 )
140                             )
141                         }
142                     }
143 
144                     else -> {
145                         // Invoke the composable for MediasetGrid. OnClick uses the navController to
146                         // navigate to the mediaset content for the mediaset that is selected by the
147                         // user.
148                         mediaGrid(
149                             items = mediaSetItems,
150                             onItemClick = { item ->
151                                 if (item is MediaGridItem.PersonMediaSetItem) {
152                                     scope.launch {
153                                         events.dispatch(
154                                             Event.LogPhotopickerUIEvent(
155                                                 FeatureToken.CATEGORY_GRID.token,
156                                                 configuration.sessionId,
157                                                 configuration.callingPackageUid ?: -1,
158                                                 Telemetry.UiEvent.CATEGORY_PEOPLEPET_OPEN,
159                                             )
160                                         )
161                                     }
162                                     navController.navigateToMediaSetContentGrid(
163                                         mediaSet = item.mediaSet
164                                     )
165                                 } else if (item is MediaGridItem.MediaSetItem) {
166                                     scope.launch {
167                                         events.dispatch(
168                                             Event.LogPhotopickerUIEvent(
169                                                 FeatureToken.CATEGORY_GRID.token,
170                                                 configuration.sessionId,
171                                                 configuration.callingPackageUid ?: -1,
172                                                 Telemetry.UiEvent.PICKER_CATEGORIES_INTERACTION,
173                                             )
174                                         )
175                                     }
176                                     navController.navigateToMediaSetContentGrid(
177                                         mediaSet = item.mediaSet
178                                     )
179                                 }
180                             },
181                             isExpandedScreen = isExpandedScreen,
182                             columns =
183                                 when (isExpandedScreen) {
184                                     true ->
185                                         GridCells.Fixed(CELLS_PER_ROW_EXPANDED_FOR_MEDIASET_GRID)
186                                     false -> GridCells.Fixed(CELLS_PER_ROW_FOR_MEDIASET_GRID)
187                                 },
188                             selection = emptySet(),
189                             gridCellPadding = MEASUREMENT_HORIZONTAL_CELL_SPACING_MEDIASET_GRID,
190                             contentPadding =
191                                 PaddingValues(MEASUREMENT_HORIZONTAL_CELL_SPACING_MEDIASET_GRID),
192                             state = state,
193                         )
194                         LaunchedEffect(Unit) {
195                             // Dispatch UI event to log loading of category contents
196                             events.dispatch(
197                                 Event.LogPhotopickerUIEvent(
198                                     FeatureToken.CATEGORY_GRID.token,
199                                     configuration.sessionId,
200                                     configuration.callingPackageUid ?: -1,
201                                     Telemetry.UiEvent.UI_LOADED_MEDIA_SETS,
202                                 )
203                             )
204                         }
205                     }
206                 }
207             }
208         }
209     }
210 }
211 
212 /**
213  * Return a generic content for the empty state.
214  *
215  * @return a [Triple] that contains the [Title, Body, Icon] for the empty state.
216  */
217 @Composable
getEmptyStateContentForMediasetnull218 private fun getEmptyStateContentForMediaset(
219     categoryType: CategoryType
220 ): Triple<String, String, ImageVector> {
221     return if (categoryType == CategoryType.PEOPLE_AND_PETS) {
222         Triple(
223             stringResource(R.string.photopicker_people_category_empty_state_title),
224             stringResource(R.string.photopicker_people_category_empty_state_body),
225             Icons.Outlined.Group,
226         )
227     } else {
228         Triple(
229             stringResource(R.string.photopicker_photos_empty_state_title),
230             stringResource(R.string.photopicker_photos_empty_state_body),
231             Icons.Outlined.Group,
232         )
233     }
234 }
235