• 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.systemui.qs.panels.ui.compose
18 
19 import android.content.ClipData
20 import androidx.compose.foundation.ExperimentalFoundationApi
21 import androidx.compose.foundation.draganddrop.dragAndDropSource
22 import androidx.compose.foundation.draganddrop.dragAndDropTarget
23 import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
24 import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
25 import androidx.compose.foundation.lazy.grid.LazyGridState
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.getValue
28 import androidx.compose.runtime.remember
29 import androidx.compose.runtime.rememberUpdatedState
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.draganddrop.DragAndDropEvent
32 import androidx.compose.ui.draganddrop.DragAndDropTarget
33 import androidx.compose.ui.draganddrop.DragAndDropTransferData
34 import androidx.compose.ui.draganddrop.mimeTypes
35 import androidx.compose.ui.draganddrop.toAndroidDragEvent
36 import androidx.compose.ui.geometry.Offset
37 import androidx.compose.ui.unit.IntRect
38 import androidx.compose.ui.unit.toRect
39 import com.android.systemui.qs.panels.shared.model.SizedTile
40 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
41 import com.android.systemui.qs.pipeline.shared.TileSpec
42 
43 /** Holds the [TileSpec] of the tile being moved and receives drag and drop events. */
44 interface DragAndDropState {
45     val draggedCell: SizedTile<EditTileViewModel>?
46     val isDraggedCellRemovable: Boolean
47     val draggedPosition: Offset
48     val dragInProgress: Boolean
49     val dragType: DragType?
50 
51     fun isMoving(tileSpec: TileSpec): Boolean
52 
53     fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType)
54 
55     fun onTargeting(target: Int, insertAfter: Boolean)
56 
57     fun onMoved(offset: Offset)
58 
59     fun movedOutOfBounds()
60 
61     fun onDrop()
62 }
63 
64 enum class DragType {
65     Add,
66     Move,
67 }
68 
69 /**
70  * Registers a composable as a [DragAndDropTarget] to receive drop events. Use this outside the tile
71  * grid to catch out of bounds drops.
72  *
73  * @param dragAndDropState The [DragAndDropState] using the tiles list
74  * @param onDrop Action to be executed when a [TileSpec] is dropped on the composable
75  */
76 @Composable
Modifiernull77 fun Modifier.dragAndDropRemoveZone(
78     dragAndDropState: DragAndDropState,
79     onDrop: (TileSpec, removalEnabled: Boolean) -> Unit,
80 ): Modifier {
81     val target =
82         remember(dragAndDropState) {
83             object : DragAndDropTarget {
84                 override fun onMoved(event: DragAndDropEvent) {
85                     dragAndDropState.onMoved(event.toOffset())
86                 }
87 
88                 override fun onDrop(event: DragAndDropEvent): Boolean {
89                     return dragAndDropState.draggedCell?.let {
90                         onDrop(it.tile.tileSpec, dragAndDropState.isDraggedCellRemovable)
91                         dragAndDropState.onDrop()
92                         true
93                     } ?: false
94                 }
95 
96                 override fun onEntered(event: DragAndDropEvent) {
97                     if (!dragAndDropState.isDraggedCellRemovable) return
98 
99                     dragAndDropState.movedOutOfBounds()
100                 }
101             }
102         }
103     return dragAndDropTarget(
104         shouldStartDragAndDrop = { event ->
105             event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE)
106         },
107         target = target,
108     )
109 }
110 
111 /**
112  * Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on the lazy tile
113  * grid to receive drag and drops events.
114  *
115  * @param gridState The [LazyGridState] of the tile list
116  * @param contentOffset The [Offset] of the tile list
117  * @param dragAndDropState The [DragAndDropState] using the tiles list
118  * @param onDrop Callback when a tile is dropped
119  */
120 @Composable
Modifiernull121 fun Modifier.dragAndDropTileList(
122     gridState: LazyGridState,
123     contentOffset: () -> Offset,
124     dragAndDropState: DragAndDropState,
125     onDrop: (TileSpec) -> Unit,
126 ): Modifier {
127     val target =
128         remember(dragAndDropState) {
129             object : DragAndDropTarget {
130                 override fun onEnded(event: DragAndDropEvent) {
131                     dragAndDropState.onDrop()
132                 }
133 
134                 override fun onMoved(event: DragAndDropEvent) {
135                     val offset = event.toOffset()
136                     dragAndDropState.onMoved(offset)
137 
138                     // Drag offset relative to the list's top left corner
139                     val relativeDragOffset = offset - contentOffset()
140                     val targetItem =
141                         gridState.layoutInfo.visibleItemsInfo.firstOrNull { item ->
142                             // Check if the drag is on this item
143                             IntRect(item.offset, item.size).toRect().contains(relativeDragOffset)
144                         }
145 
146                     targetItem?.let {
147                         dragAndDropState.onTargeting(it.index, insertAfter(it, relativeDragOffset))
148                     }
149                 }
150 
151                 override fun onDrop(event: DragAndDropEvent): Boolean {
152                     return dragAndDropState.draggedCell?.let {
153                         onDrop(it.tile.tileSpec)
154                         dragAndDropState.onDrop()
155                         true
156                     } ?: false
157                 }
158             }
159         }
160     return dragAndDropTarget(
161         target = target,
162         shouldStartDragAndDrop = { event ->
163             event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE)
164         },
165     )
166 }
167 
DragAndDropEventnull168 private fun DragAndDropEvent.toOffset(): Offset {
169     return toAndroidDragEvent().run { Offset(x, y) }
170 }
171 
insertAfternull172 private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean {
173     // We want to insert the tile after the target if we're aiming at the end of a large tile
174     // TODO(ostonge): Verify this behavior in RTL
175     val itemCenter = item.offset.x + item.size.width * .75
176     return item.span != 1 && offset.x > itemCenter
177 }
178 
179 @OptIn(ExperimentalFoundationApi::class)
180 @Composable
dragAndDropTileSourcenull181 fun Modifier.dragAndDropTileSource(
182     sizedTile: SizedTile<EditTileViewModel>,
183     dragAndDropState: DragAndDropState,
184     dragType: DragType,
185     onDragStart: () -> Unit,
186 ): Modifier {
187     val dragState by rememberUpdatedState(dragAndDropState)
188     @Suppress("DEPRECATION") // b/368361871
189     return dragAndDropSource(
190         block = {
191             detectDragGesturesAfterLongPress(
192                 onDrag = { _, _ -> },
193                 onDragStart = {
194                     dragState.onStarted(sizedTile, dragType)
195                     onDragStart()
196 
197                     // The tilespec from the ClipData transferred isn't actually needed as we're
198                     // moving a tile within the same application. We're using a custom MIME type to
199                     // limit the drag event to QS.
200                     startTransfer(
201                         DragAndDropTransferData(
202                             ClipData(
203                                 QsDragAndDrop.CLIPDATA_LABEL,
204                                 arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
205                                 ClipData.Item(sizedTile.tile.tileSpec.spec),
206                             )
207                         )
208                     )
209                 },
210             )
211         }
212     )
213 }
214 
215 private object QsDragAndDrop {
216     const val CLIPDATA_LABEL = "tilespec"
217     const val TILESPEC_MIME_TYPE = "qstile/tilespec"
218 }
219