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.systemui.qs.panels.ui.compose
18 
19 import androidx.compose.foundation.layout.Arrangement
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.Column
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.fillMaxWidth
26 import androidx.compose.foundation.layout.height
27 import androidx.compose.foundation.layout.padding
28 import androidx.compose.foundation.lazy.grid.GridCells
29 import androidx.compose.foundation.lazy.grid.GridItemSpan
30 import androidx.compose.foundation.lazy.grid.LazyGridScope
31 import androidx.compose.foundation.rememberScrollState
32 import androidx.compose.foundation.shape.RoundedCornerShape
33 import androidx.compose.foundation.verticalScroll
34 import androidx.compose.material3.MaterialTheme
35 import androidx.compose.material3.Switch
36 import androidx.compose.material3.Text
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.DisposableEffect
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.rememberUpdatedState
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.draw.drawWithContent
43 import androidx.compose.ui.graphics.Color
44 import androidx.compose.ui.graphics.Path
45 import androidx.compose.ui.graphics.PathEffect
46 import androidx.compose.ui.graphics.Shape
47 import androidx.compose.ui.graphics.addOutline
48 import androidx.compose.ui.graphics.drawscope.Stroke
49 import androidx.compose.ui.res.dimensionResource
50 import androidx.compose.ui.text.font.FontWeight
51 import androidx.compose.ui.unit.Dp
52 import androidx.compose.ui.unit.dp
53 import androidx.lifecycle.compose.collectAsStateWithLifecycle
54 import com.android.compose.modifiers.background
55 import com.android.systemui.dagger.SysUISingleton
56 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
57 import com.android.systemui.qs.panels.ui.viewmodel.PartitionedGridViewModel
58 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
59 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
60 import com.android.systemui.qs.pipeline.shared.TileSpec
61 import com.android.systemui.res.R
62 import javax.inject.Inject
63 
64 @SysUISingleton
65 class PartitionedGridLayout @Inject constructor(private val viewModel: PartitionedGridViewModel) :
66     GridLayout {
67     @Composable
68     override fun TileGrid(tiles: List<TileViewModel>, modifier: Modifier) {
69         DisposableEffect(tiles) {
70             val token = Any()
71             tiles.forEach { it.startListening(token) }
72             onDispose { tiles.forEach { it.stopListening(token) } }
73         }
74         val columns by viewModel.columns.collectAsStateWithLifecycle()
75         val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
76         val largeTileHeight = tileHeight()
77         val iconTileHeight = tileHeight(showLabels)
78         val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) }
79 
80         TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
81             // Large tiles
82             items(largeTiles.size, span = { GridItemSpan(2) }) { index ->
83                 Tile(
84                     tile = largeTiles[index],
85                     iconOnly = false,
86                     modifier = Modifier.height(largeTileHeight)
87                 )
88             }
89             fillUpRow(nTiles = largeTiles.size, columns = columns / 2)
90 
91             // Small tiles
92             items(smallTiles.size) { index ->
93                 Tile(
94                     tile = smallTiles[index],
95                     iconOnly = true,
96                     showLabels = showLabels,
97                     modifier = Modifier.height(iconTileHeight)
98                 )
99             }
100         }
101     }
102 
103     @Composable
104     override fun EditTileGrid(
105         tiles: List<EditTileViewModel>,
106         modifier: Modifier,
107         onAddTile: (TileSpec, Int) -> Unit,
108         onRemoveTile: (TileSpec) -> Unit
109     ) {
110         val columns by viewModel.columns.collectAsStateWithLifecycle()
111         val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
112 
113         val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
114         val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
115             onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
116         }
117         val largeTileHeight = tileHeight()
118         val iconTileHeight = tileHeight(showLabels)
119         val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
120 
121         Column(
122             verticalArrangement = Arrangement.spacedBy(tilePadding),
123             modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState())
124         ) {
125             Row(
126                 modifier =
127                     Modifier.background(
128                             color = MaterialTheme.colorScheme.surfaceVariant,
129                             alpha = { 1f },
130                             shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))
131                         )
132                         .padding(tilePadding)
133             ) {
134                 Column(Modifier.padding(start = tilePadding)) {
135                     Text(
136                         text = "Show text labels",
137                         color = MaterialTheme.colorScheme.onBackground,
138                         fontWeight = FontWeight.Bold
139                     )
140                     Text(
141                         text = "Display names under each tile",
142                         color = MaterialTheme.colorScheme.onBackground
143                     )
144                 }
145                 Spacer(modifier = Modifier.weight(1f))
146                 Switch(checked = showLabels, onCheckedChange = { viewModel.setShowLabels(it) })
147             }
148 
149             CurrentTiles(
150                 tiles = currentTiles,
151                 largeTileHeight = largeTileHeight,
152                 iconTileHeight = iconTileHeight,
153                 tilePadding = tilePadding,
154                 onRemoveTile = onRemoveTile,
155                 isIconOnly = viewModel::isIconTile,
156                 columns = columns,
157                 showLabels = showLabels,
158             )
159             AvailableTiles(
160                 tiles = otherTiles,
161                 largeTileHeight = largeTileHeight,
162                 iconTileHeight = iconTileHeight,
163                 tilePadding = tilePadding,
164                 addTileToEnd = addTileToEnd,
165                 isIconOnly = viewModel::isIconTile,
166                 showLabels = showLabels,
167                 columns = columns,
168             )
169         }
170     }
171 
172     @Composable
173     private fun CurrentTiles(
174         tiles: List<EditTileViewModel>,
175         largeTileHeight: Dp,
176         iconTileHeight: Dp,
177         tilePadding: Dp,
178         onRemoveTile: (TileSpec) -> Unit,
179         isIconOnly: (TileSpec) -> Boolean,
180         showLabels: Boolean,
181         columns: Int,
182     ) {
183         val (smallTiles, largeTiles) = tiles.partition { isIconOnly(it.tileSpec) }
184 
185         val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding)
186         val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding)
187 
188         CurrentTilesContainer {
189             TileLazyGrid(
190                 columns = GridCells.Fixed(columns),
191                 modifier = Modifier.height(largeGridHeight),
192             ) {
193                 editTiles(
194                     largeTiles,
195                     ClickAction.REMOVE,
196                     onRemoveTile,
197                     { false },
198                     indicatePosition = true
199                 )
200             }
201         }
202         CurrentTilesContainer {
203             TileLazyGrid(
204                 columns = GridCells.Fixed(columns),
205                 modifier = Modifier.height(smallGridHeight),
206             ) {
207                 editTiles(
208                     smallTiles,
209                     ClickAction.REMOVE,
210                     onRemoveTile,
211                     { true },
212                     showLabels = showLabels,
213                     indicatePosition = true
214                 )
215             }
216         }
217     }
218 
219     @Composable
220     private fun AvailableTiles(
221         tiles: List<EditTileViewModel>,
222         largeTileHeight: Dp,
223         iconTileHeight: Dp,
224         tilePadding: Dp,
225         addTileToEnd: (TileSpec) -> Unit,
226         isIconOnly: (TileSpec) -> Boolean,
227         showLabels: Boolean,
228         columns: Int,
229     ) {
230         val (tilesStock, tilesCustom) = tiles.partition { it.appName == null }
231         val (smallTiles, largeTiles) = tilesStock.partition { isIconOnly(it.tileSpec) }
232 
233         val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding)
234         val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding)
235         val largeGridHeightCustom =
236             gridHeight(tilesCustom.size, iconTileHeight, columns, tilePadding)
237 
238         // Add up the height of all three grids and add padding in between
239         val gridHeight =
240             largeGridHeight + smallGridHeight + largeGridHeightCustom + (tilePadding * 2)
241 
242         AvailableTilesContainer {
243             TileLazyGrid(
244                 columns = GridCells.Fixed(columns),
245                 modifier = Modifier.height(gridHeight),
246             ) {
247                 // Large tiles
248                 editTiles(largeTiles, ClickAction.ADD, addTileToEnd, isIconOnly)
249                 fillUpRow(nTiles = largeTiles.size, columns = columns / 2)
250 
251                 // Small tiles
252                 editTiles(
253                     smallTiles,
254                     ClickAction.ADD,
255                     addTileToEnd,
256                     isIconOnly,
257                     showLabels = showLabels
258                 )
259                 fillUpRow(nTiles = smallTiles.size, columns = columns)
260 
261                 // Custom tiles, all icons
262                 editTiles(
263                     tilesCustom,
264                     ClickAction.ADD,
265                     addTileToEnd,
266                     isIconOnly,
267                     showLabels = showLabels
268                 )
269             }
270         }
271     }
272 
273     @Composable
274     private fun CurrentTilesContainer(content: @Composable () -> Unit) {
275         Box(
276             Modifier.fillMaxWidth()
277                 .dashedBorder(
278                     color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
279                     shape = Dimensions.ContainerShape,
280                 )
281                 .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
282         ) {
283             content()
284         }
285     }
286 
287     @Composable
288     private fun AvailableTilesContainer(content: @Composable () -> Unit) {
289         Box(
290             Modifier.fillMaxWidth()
291                 .background(
292                     color = MaterialTheme.colorScheme.background,
293                     alpha = { 1f },
294                     shape = Dimensions.ContainerShape,
295                 )
296                 .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
297         ) {
298             content()
299         }
300     }
301 
302     private fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp {
303         val rows = (nTiles + columns - 1) / columns
304         return ((tileHeight + padding) * rows) - padding
305     }
306 
307     /** Fill up the rest of the row if it's not complete. */
308     private fun LazyGridScope.fillUpRow(nTiles: Int, columns: Int) {
309         if (nTiles % columns != 0) {
310             item(span = { GridItemSpan(maxCurrentLineSpan) }) { Spacer(Modifier) }
311         }
312     }
313 
314     private fun Modifier.dashedBorder(
315         color: Color,
316         shape: Shape,
317     ): Modifier {
318         return this.drawWithContent {
319             val outline = shape.createOutline(size, layoutDirection, this)
320             val path = Path()
321             path.addOutline(outline)
322             val stroke =
323                 Stroke(
324                     width = 1.dp.toPx(),
325                     pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
326                 )
327             this.drawContent()
328             drawPath(path = path, style = stroke, color = color)
329         }
330     }
331 
332     private object Dimensions {
333         // Corner radius is half the height of a tile + padding
334         val ContainerShape = RoundedCornerShape(48.dp)
335     }
336 }
337