Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
QtDragManager.java
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5package org.qtproject.qt.android;
6
7import android.app.Activity;
8import android.content.ClipData;
9import android.content.ClipDescription;
10import android.graphics.Bitmap;
11import android.graphics.Canvas;
12import android.graphics.Point;
13import android.net.Uri;
14import android.util.Log;
15import android.view.DragAndDropPermissions;
16import android.view.DragEvent;
17import android.view.View;
18
19import java.util.Arrays;
20
21class QtDragManager implements View.OnDragListener
22{
23 private static final String TAG = "QtDragManager";
24 private static final String DEFAULT_MIME_TYPE = "application/octet-stream";
25
26 private static QtDragManager m_instance = null;
27
28 private volatile long m_nativePointer = 0; // QAndroidPlatformDrag pointer
29 private volatile View m_sourceView = null;
30 private DragAndDropPermissions m_dragPermissions = null;
31
32 static native boolean onDragEvent(long nativePointer, int viewId, int action, float x, float y,
33 String[] mimeTypes, String[] clipData, boolean result);
34
35 @UsedFromNativeCode
36 static synchronized QtDragManager getInstance()
37 {
38 if (m_instance == null)
39 m_instance = new QtDragManager();
40 return m_instance;
41 }
42
43 @UsedFromNativeCode
44 static synchronized void setNativePointer(long nativePointer)
45 {
46 getInstance().m_nativePointer = nativePointer;
47 }
48
49 @UsedFromNativeCode
50 static synchronized void clearNativePointer(long expected)
51 {
52 final QtDragManager instance = getInstance();
53 if (instance.m_nativePointer == expected) {
54 instance.m_nativePointer = 0;
55 // Drop the source reference so a torn-down drag is not kept alive by
56 // the process-lifetime singleton. Permissions are released on drop or
57 // drag-ended and auto-revoked when the activity is destroyed.
58 instance.m_sourceView = null;
59 }
60 }
61
62 @UsedFromNativeCode
63 void startDrag(QtWindow sourceWindow, String[] mimeTypes, String[] clipData,
64 Bitmap shadowBitmap, int hotSpotX, int hotSpotY)
65 {
66 if (sourceWindow == null)
67 return;
68
69 // Post even when inactive so the drag loop is never left waiting on a parked action.
70 QtNative.runAction(() -> {
71 boolean started = false;
72 try {
73 String text = null;
74 String html = null;
75 Uri uri = null;
76 String uriListData = null;
77 boolean hasContentUri = false;
78 for (int i = 0; i < mimeTypes.length; ++i) {
79 final String data = clipData[i];
80 final String mime = mimeTypes[i];
81 if (mime.equals(ClipDescription.MIMETYPE_TEXT_HTML)) {
82 html = data;
83 } else if (mime.equals(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
84 text = data;
85 } else if (mime.equals(ClipDescription.MIMETYPE_TEXT_URILIST)
86 && !data.isEmpty()) {
87 final String first = data.split("[\\r\\n]+", 2)[0].trim();
88 if (!first.isEmpty()) {
89 uriListData = data;
90 uri = Uri.parse(first);
91 hasContentUri = "content".equals(uri.getScheme());
92 }
93 }
94 }
95
96 boolean hasMimeTypes = mimeTypes.length > 0;
97 String[] types = hasMimeTypes ? mimeTypes : new String[]{ DEFAULT_MIME_TYPE };
98 ClipData.Item item = new ClipData.Item(text, html, null, uri);
99 ClipData clip = new ClipData(new ClipDescription("DragClip", types), item);
100 // Add one item per extra URI so receivers that iterate items see all files.
101 if (uriListData != null) {
102 final String[] parts = uriListData.split("[\\r\\n]+");
103 for (int i = 1; i < parts.length; ++i) {
104 final String part = parts[i].trim();
105 if (!part.isEmpty()) {
106 final Uri extraUri = Uri.parse(part);
107 hasContentUri |= "content".equals(extraUri.getScheme());
108 clip.addItem(new ClipData.Item(extraUri));
109 }
110 }
111 }
112 View.DragShadowBuilder shadow = new QtDragShadowBuilder(sourceWindow, shadowBitmap,
113 hotSpotX, hotSpotY);
114 m_sourceView = sourceWindow;
115
116 // Cross into other apps, granting read access only for content URIs.
117 int flags = View.DRAG_FLAG_GLOBAL;
118 if (hasContentUri)
119 flags |= View.DRAG_FLAG_GLOBAL_URI_READ;
120
121 started = sourceWindow.startDragAndDrop(clip, shadow, QtDragManager.this, flags);
122 } catch (Exception e) {
123 Log.e(TAG, "startDragAndDrop() failed on window id " + sourceWindow.getId(), e);
124 }
125
126 // Wake the native drag loop if the drag never started, otherwise drag() hangs.
127 if (!started) {
128 m_sourceView = null;
129 final long ptr = m_nativePointer;
130 if (ptr != 0) {
131 final String[] empty = new String[0];
132 onDragEvent(ptr, sourceWindow.getId(), DragEvent.ACTION_DRAG_ENDED,
133 0f, 0f, empty, empty, false);
134 }
135 }
136 }, false);
137 }
138
139 @UsedFromNativeCode
140 void cancelDrag()
141 {
142 QtNative.runAction(() -> {
143 if (m_sourceView != null)
144 m_sourceView.cancelDragAndDrop();
145 });
146 }
147
148 private void releaseDragPermissions()
149 {
150 if (m_dragPermissions != null) {
151 m_dragPermissions.release();
152 m_dragPermissions = null;
153 }
154 }
155
156 void onSourceWindowDetached(View view)
157 {
158 if (m_sourceView != view)
159 return;
160 m_sourceView = null;
161 final long ptr = m_nativePointer;
162 if (ptr != 0) {
163 final String[] empty = new String[0];
164 onDragEvent(ptr, view.getId(), DragEvent.ACTION_DRAG_ENDED, 0f, 0f, empty, empty, false);
165 }
166 }
167
168 @Override
169 public boolean onDrag(View view, DragEvent event)
170 {
171 final int action = event.getAction();
172
173 if (action == DragEvent.ACTION_DRAG_ENDED) {
174 m_sourceView = null;
175 releaseDragPermissions();
176 }
177
178 final long nativePointer = m_nativePointer;
179 if (nativePointer == 0)
180 return false;
181
182 if (action == DragEvent.ACTION_DRAG_STARTED)
183 return true;
184
185 String[] mimeTypes = new String[0];
186 String[] clipData = new String[0];
187 final ClipDescription description = event.getClipDescription();
188 if (description != null) {
189 final int size = description.getMimeTypeCount();
190 mimeTypes = new String[size];
191 for (int i = 0; i < size; ++i)
192 mimeTypes[i] = description.getMimeType(i);
193 }
194
195 // Clip data is only readable on ACTION_DROP and move events carry types only.
196 if (action == DragEvent.ACTION_DROP) {
197 try {
198 final ClipData clip = event.getClipData();
199 if (clip != null && clip.getItemCount() > 0) {
200 clipData = new String[mimeTypes.length];
201 Arrays.fill(clipData, "");
202 // URIs span all items (one per file)
203 StringBuilder uriList = new StringBuilder();
204 for (int i = 0; i < clip.getItemCount(); ++i) {
205 Uri itemUri = clip.getItemAt(i).getUri();
206 if (itemUri != null) {
207 if (uriList.length() > 0)
208 uriList.append('\n');
209 uriList.append(itemUri.toString());
210 }
211 }
212 if (uriList.length() > 0) {
213 releaseDragPermissions();
214 final Activity activity = QtNative.activity();
215 if (activity != null) {
216 m_dragPermissions = activity.requestDragAndDropPermissions(event);
217 if (m_dragPermissions == null)
218 Log.w(TAG, "Drag and drop reading permissions denied.");
219 }
220 }
221
222 // text/HTML are in item 0 only
223 ClipData.Item item = clip.getItemAt(0);
224 for (int i = 0; i < mimeTypes.length; ++i) {
225 if (ClipDescription.MIMETYPE_TEXT_HTML.equals(mimeTypes[i])) {
226 final String html = item.getHtmlText();
227 clipData[i] = (html != null) ? html : "";
228 } else if (ClipDescription.MIMETYPE_TEXT_URILIST.equals(mimeTypes[i])) {
229 clipData[i] = uriList.toString();
230 } else if (ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeTypes[i])) {
231 // An item can carry both a URI and a text label, so prefer the label.
232 CharSequence text = item.getText();
233 if (text == null)
234 text = item.coerceToText(view.getContext());
235 clipData[i] = (text != null) ? text.toString() : "";
236 } else {
237 final Uri itemUri = item.getUri();
238 if (itemUri != null) {
239 clipData[i] = itemUri.toString();
240 } else {
241 final CharSequence text = item.coerceToText(view.getContext());
242 clipData[i] = (text != null) ? text.toString() : "";
243 }
244 }
245 }
246 }
247 } catch (Exception e) {
248 Log.e(TAG, "Failed to read dropped clip data", e);
249 }
250 }
251
252 return onDragEvent(nativePointer, view.getId(), action, event.getX(), event.getY(),
253 mimeTypes, clipData, event.getResult());
254 }
255
256 private static class QtDragShadowBuilder extends View.DragShadowBuilder
257 {
258 private final Bitmap m_bitmap;
259 private final int m_hotSpotX;
260 private final int m_hotSpotY;
261
262 QtDragShadowBuilder(View view, Bitmap bitmap, int hotSpotX, int hotSpotY)
263 {
264 super(view);
265 m_bitmap = bitmap;
266 m_hotSpotX = hotSpotX;
267 m_hotSpotY = hotSpotY;
268 }
269
270 @Override
271 public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint)
272 {
273 if (m_bitmap == null) {
274 super.onProvideShadowMetrics(outShadowSize, outShadowTouchPoint);
275 return;
276 }
277
278 outShadowSize.set(m_bitmap.getWidth(), m_bitmap.getHeight());
279 outShadowTouchPoint.set(m_hotSpotX, m_hotSpotY);
280 }
281
282 @Override
283 public void onDrawShadow(Canvas canvas)
284 {
285 if (m_bitmap != null)
286 canvas.drawBitmap(m_bitmap, 0, 0, null);
287 }
288 }
289}
QJSValue expected
Definition qjsengine.cpp:12
Q_CORE_EXPORT QtJniTypes::Activity activity()
static ControlElement< T > * ptr(QWidget *widget)
GLint GLint GLint GLint GLint x
GLsizei GLenum GLenum * types
GLenum GLuint GLintptr GLsizeiptr size
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
[0]
GLint first
GLfloat GLfloat f
[26]
GLbitfield flags
GLint y
struct _cl_event * event
GLsizei GLfixed GLfixed GLfixed GLfixed const GLubyte * bitmap
GLuint64EXT * result
[6]
QQuickView * view
[0]