• 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.gestures.detectHorizontalDragGestures
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.PaddingValues
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.Spacer
24 import androidx.compose.foundation.layout.fillMaxSize
25 import androidx.compose.foundation.layout.size
26 import androidx.compose.foundation.layout.width
27 import androidx.compose.foundation.lazy.grid.GridCells
28 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
29 import androidx.compose.material3.Icon
30 import androidx.compose.material3.Text
31 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.LaunchedEffect
34 import androidx.compose.runtime.getValue
35 import androidx.compose.runtime.rememberCoroutineScope
36 import androidx.compose.ui.Alignment
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.graphics.vector.ImageVector
39 import androidx.compose.ui.input.pointer.pointerInput
40 import androidx.compose.ui.res.stringResource
41 import androidx.compose.ui.res.vectorResource
42 import androidx.compose.ui.semantics.contentDescription
43 import androidx.compose.ui.text.style.TextOverflow
44 import androidx.compose.ui.unit.dp
45 import androidx.lifecycle.compose.collectAsStateWithLifecycle
46 import androidx.paging.compose.collectAsLazyPagingItems
47 import com.android.photopicker.R
48 import com.android.photopicker.core.components.MediaGridItem
49 import com.android.photopicker.core.components.mediaGrid
50 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
51 import com.android.photopicker.core.events.Event
52 import com.android.photopicker.core.events.LocalEvents
53 import com.android.photopicker.core.events.Telemetry
54 import com.android.photopicker.core.features.FeatureToken
55 import com.android.photopicker.core.features.LocalFeatureManager
56 import com.android.photopicker.core.navigation.LocalNavController
57 import com.android.photopicker.core.navigation.PhotopickerDestinations
58 import com.android.photopicker.core.obtainViewModel
59 import com.android.photopicker.core.theme.LocalWindowSizeClass
60 import com.android.photopicker.extensions.navigateToAlbumMediaGridForCategories
61 import com.android.photopicker.extensions.navigateToCategoryGrid
62 import com.android.photopicker.extensions.navigateToMediaSetGrid
63 import com.android.photopicker.extensions.navigateToPhotoGrid
64 import com.android.photopicker.features.navigationbar.NavigationBarButton
65 import com.android.photopicker.features.photogrid.PhotoGridFeature
66 import com.android.photopicker.features.search.SearchFeature
67 import kotlinx.coroutines.launch
68 
69 /** The number of grid cells per row for Phone / narrow layouts */
70 private val CELLS_PER_ROW_FOR_CATEGORY_GRID = 2
71 
72 /** The number of grid cells per row for Tablet / expanded layouts */
73 private val CELLS_PER_ROW_EXPANDED_FOR_CATEGORY_GRID = 3
74 
75 /** The amount of padding to use around each cell in the categories grid. */
76 private val MEASUREMENT_HORIZONTAL_CELL_SPACING_CATEGORY_GRID = 16.dp
77 
78 /**
79  * Primary composable for drawing the main Category Grid on [PhotopickerDestinations.ALBUM_GRID]
80  *
81  * @param viewModel - A viewModel override for the composable. Normally, this is fetched via hilt
82  *   from the backstack entry by using obtainViewModel()
83  */
84 @Composable
85 fun CategoryGrid(viewModel: CategoryGridViewModel = obtainViewModel()) {
86     val items = viewModel.getCategoriesAndAlbums().collectAsLazyPagingItems()
87     val state = rememberLazyGridState()
88     val navController = LocalNavController.current
89     val featureManager = LocalFeatureManager.current
90     val configuration = LocalPhotopickerConfiguration.current
91     val events = LocalEvents.current
92     val scope = rememberCoroutineScope()
93 
94     // Use the expanded layout any time the Width is Medium or larger.
95     val isExpandedScreen: Boolean =
96         when (LocalWindowSizeClass.current.widthSizeClass) {
97             WindowWidthSizeClass.Medium -> true
98             WindowWidthSizeClass.Expanded -> true
99             else -> false
100         }
101 
102     val previouslySelectedItem by viewModel.previouslySelectedItem.collectAsStateWithLifecycle()
103     Column(
104         modifier =
105             Modifier.fillMaxSize().pointerInput(Unit) {
106                 detectHorizontalDragGestures(
107                     onHorizontalDrag = { _, dragAmount ->
108                         // This may need some additional fine tuning by looking at a certain
109                         // distance in dragAmount, but initial testing suggested this worked
110                         // pretty well as is.
111                         if (dragAmount > 0) {
112                             // Positive is a right swipe
113                             if (featureManager.isFeatureEnabled(PhotoGridFeature::class.java)) {
114                                 navController.navigateToPhotoGrid()
115                                 // Dispatch UI event to indicate switching to photos tab
116                                 scope.launch {
117                                     events.dispatch(
118                                         Event.LogPhotopickerUIEvent(
119                                             FeatureToken.CATEGORY_GRID.token,
120                                             configuration.sessionId,
121                                             configuration.callingPackageUid ?: -1,
122                                             Telemetry.UiEvent.SWITCH_PICKER_TAB,
123                                         )
124                                     )
125                                 }
126                             }
127                         }
128                     }
129                 )
130             }
131     ) {
132         // Invoke the composable for Category Grid. OnClick uses the navController to navigate to
133         // the category content for the category that is selected by the user.
134         mediaGrid(
135             items = items,
136             focusItem = previouslySelectedItem,
137             onItemClick = { item ->
138                 if (item is MediaGridItem.AlbumItem) {
139                     // Dispatch events to log album related details
140                     scope.launch {
141                         events.dispatch(
142                             Event.LogPhotopickerAlbumOpenedUIEvent(
143                                 FeatureToken.CATEGORY_GRID.token,
144                                 configuration.sessionId,
145                                 configuration.callingPackageUid ?: -1,
146                                 item.album,
147                             )
148                         )
149                         events.dispatch(
150                             Event.LogPhotopickerUIEvent(
151                                 FeatureToken.CATEGORY_GRID.token,
152                                 configuration.sessionId,
153                                 configuration.callingPackageUid ?: -1,
154                                 Telemetry.UiEvent.PICKER_ALBUMS_INTERACTION,
155                             )
156                         )
157                     }
158                     viewModel.setPreviouslySelectedItem(item)
159                     navController.navigateToAlbumMediaGridForCategories(album = item.album)
160                 } else if (item is MediaGridItem.CategoryItem) {
161                     scope.launch {
162                         events.dispatch(
163                             Event.LogPhotopickerUIEvent(
164                                 FeatureToken.CATEGORY_GRID.token,
165                                 configuration.sessionId,
166                                 configuration.callingPackageUid ?: -1,
167                                 Telemetry.UiEvent.CATEGORY_MEDIA_SETS_OPEN,
168                             )
169                         )
170                     }
171                     viewModel.setPreviouslySelectedItem(item)
172                     navController.navigateToMediaSetGrid(category = item.category)
173                 }
174             },
175             isExpandedScreen = isExpandedScreen,
176             columns =
177                 when (isExpandedScreen) {
178                     true -> GridCells.Fixed(CELLS_PER_ROW_EXPANDED_FOR_CATEGORY_GRID)
179                     false -> GridCells.Fixed(CELLS_PER_ROW_FOR_CATEGORY_GRID)
180                 },
181             selection = emptySet(),
182             gridCellPadding = MEASUREMENT_HORIZONTAL_CELL_SPACING_CATEGORY_GRID,
183             contentPadding = PaddingValues(MEASUREMENT_HORIZONTAL_CELL_SPACING_CATEGORY_GRID),
184             state = state,
185         )
186         LaunchedEffect(Unit) {
187             // Dispatch UI event to denote loading of media categories and albums
188             scope.launch {
189                 events.dispatch(
190                     Event.LogPhotopickerUIEvent(
191                         FeatureToken.CATEGORY_GRID.token,
192                         configuration.sessionId,
193                         configuration.callingPackageUid ?: -1,
194                         Telemetry.UiEvent.UI_LOADED_CATEGORIES_AND_ALBUMS,
195                     )
196                 )
197             }
198         }
199     }
200 }
201 
202 /**
203  * The navigation button for the main category grid. Composable for
204  * [Location.NAVIGATION_BAR_NAV_BUTTON]
205  */
206 @Composable
CategoryButtonnull207 fun CategoryButton(modifier: Modifier) {
208     val navController = LocalNavController.current
209     val scope = rememberCoroutineScope()
210     val events = LocalEvents.current
211     val sessionId = LocalPhotopickerConfiguration.current.sessionId
212     val packageUid = LocalPhotopickerConfiguration.current.callingPackageUid ?: -1
213     val featureManager = LocalFeatureManager.current
214     val searchFeatureEnabled = featureManager.isFeatureEnabled(SearchFeature::class.java)
215 
216     NavigationBarButton(
217         onClick = {
218             // Dispatch UI event to denote switching to category tab
219             scope.launch {
220                 events.dispatch(
221                     Event.LogPhotopickerUIEvent(
222                         FeatureToken.CATEGORY_GRID.token,
223                         sessionId,
224                         packageUid,
225                         Telemetry.UiEvent.SWITCH_PICKER_TAB,
226                     )
227                 )
228             }
229             navController.navigateToCategoryGrid()
230         },
231         modifier = modifier,
232         isCurrentRoute = { route -> route == PhotopickerDestinations.ALBUM_GRID.route },
233     ) {
234         when {
235             searchFeatureEnabled -> {
236                 Row(verticalAlignment = Alignment.CenterVertically) {
237                     Icon(
238                         imageVector =
239                             ImageVector.vectorResource(R.drawable.photopicker_category_icon),
240                         contentDescription = null,
241                         modifier = Modifier.size(18.dp),
242                     )
243                     Spacer(Modifier.width(8.dp))
244                     Text(
245                         stringResource(R.string.photopicker_categories_nav_button_label),
246                         maxLines = 1, // Limit the text to a single line
247                         overflow = TextOverflow.Ellipsis,
248                     )
249                 }
250             }
251             else ->
252                 Text(
253                     stringResource(R.string.photopicker_categories_nav_button_label),
254                     maxLines = 1, // Limit the text to a single line
255                     overflow = TextOverflow.Ellipsis,
256                 )
257         }
258     }
259 }
260