1 /*
2  * Copyright 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 androidx.compose.foundation.content
18 
19 import android.content.ClipData
20 import android.net.Uri
21 import android.view.DragEvent
22 import android.view.View
23 import androidx.compose.foundation.text.input.internal.DragAndDropTestUtils
24 import androidx.compose.ui.geometry.Offset
25 import androidx.compose.ui.unit.Density
26 
27 /** A helper to test multi-window Drag And Drop interactions. */
testDragAndDropnull28 internal fun testDragAndDrop(view: View, density: Density, block: DragAndDropScope.() -> Unit) {
29     DragAndDropScopeImpl(view, density).block()
30 }
31 
32 internal interface DragAndDropScope : Density {
33 
34     /** Drags an item with ClipData that only holds the given [text] to the [offset] location. */
dragnull35     fun drag(offset: Offset, text: String): Boolean
36 
37     /** Drags an item with ClipData that only holds the given [uri] to the [offset] location. */
38     fun drag(offset: Offset, uri: Uri): Boolean
39 
40     /** Drags an item with [clipData] payload to the [offset] location. */
41     fun drag(offset: Offset, clipData: ClipData): Boolean
42 
43     /** Drops the previously declared dragging item. */
44     fun drop(): Boolean
45 
46     /** Cancels the ongoing drag without dropping it. */
47     fun cancelDrag()
48 }
49 
50 @Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
51 private class DragAndDropScopeImpl(val view: View, density: Density) :
52     DragAndDropScope, Density by density {
53     private var lastDraggingOffsetAndItem: Pair<Offset, Any>? = null
54 
55     override fun drag(offset: Offset, text: String): Boolean = dragAny(offset, text)
56 
57     override fun drag(offset: Offset, uri: Uri): Boolean = dragAny(offset, uri)
58 
59     override fun drag(offset: Offset, clipData: ClipData): Boolean = dragAny(offset, clipData)
60 
61     /** @param item Can be only [String], [Uri], or [ClipData]. */
62     private fun dragAny(offset: Offset, item: Any): Boolean {
63         val _lastDraggingItem = lastDraggingOffsetAndItem
64         var result = false
65         if (_lastDraggingItem == null || _lastDraggingItem.second != item) {
66             result =
67                 view.dispatchDragEvent(
68                     makeDragEvent(action = DragEvent.ACTION_DRAG_STARTED, item = item)
69                 ) || result
70         }
71         result =
72             view.dispatchDragEvent(
73                 makeDragEvent(action = DragEvent.ACTION_DRAG_LOCATION, item = item, offset = offset)
74             ) || result
75         lastDraggingOffsetAndItem = offset to item
76         return result
77     }
78 
79     override fun drop(): Boolean {
80         val lastDraggingOffsetAndItem = lastDraggingOffsetAndItem
81         check(lastDraggingOffsetAndItem != null) { "There are no ongoing dragging event to drop" }
82         val (lastDraggingOffset, lastDraggingItem) = lastDraggingOffsetAndItem
83 
84         return view.dispatchDragEvent(
85             makeDragEvent(
86                 DragEvent.ACTION_DROP,
87                 item = lastDraggingItem,
88                 offset = lastDraggingOffset
89             )
90         )
91     }
92 
93     override fun cancelDrag() {
94         lastDraggingOffsetAndItem = null
95         view.dispatchDragEvent(DragAndDropTestUtils.makeTextDragEvent(DragEvent.ACTION_DRAG_ENDED))
96     }
97 
98     private fun makeDragEvent(action: Int, item: Any, offset: Offset = Offset.Zero): DragEvent {
99         return when (item) {
100             is String -> {
101                 DragAndDropTestUtils.makeTextDragEvent(action, item, offset)
102             }
103             is Uri -> {
104                 DragAndDropTestUtils.makeImageDragEvent(action, item, offset)
105             }
106             is ClipData -> {
107                 DragAndDropTestUtils.makeDragEvent(action, item, offset)
108             }
109             else -> {
110                 throw IllegalArgumentException(
111                     "{item=$item} can only be one of [String], [Uri], or [ClipData]"
112                 )
113             }
114         }
115     }
116 }
117