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