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
qandroidplatformdrag.cpp
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
6
7#if QT_CONFIG(draganddrop)
8
9#include "androidjnimain.h"
10#include "qandroidplatformwindow.h"
11
12#include <QtCore/QEventLoop>
13#include <QtCore/QJniEnvironment>
14#include <QtCore/QJniObject>
15#include <QtCore/QLoggingCategory>
16#include <QtCore/QMetaMethod>
17#include <QtCore/QMimeData>
18#include <QtCore/QThread>
19#include <QtCore/QUrl>
20#include <QtCore/qcoreapplication_platform.h>
21#include <QtCore/private/qjnihelpers_p.h>
22#include <QtGui/QDrag>
23#include <QtGui/QGuiApplication>
24#include <QtGui/QPixmap>
25#include <qpa/qwindowsysteminterface.h>
26
27QT_BEGIN_NAMESPACE
28
29using namespace Qt::StringLiterals;
30using namespace QtJniTypes;
31
32Q_DECLARE_JNI_CLASS(Bitmap, "android/graphics/Bitmap")
33Q_DECLARE_JNI_CLASS(FileProvider, "androidx/core/content/FileProvider")
34
35Q_LOGGING_CATEGORY(lcQpaDrag, "qt.qpa.drag")
36
37QAndroidPlatformDrag *QAndroidPlatformDrag::s_currentDrag = nullptr;
38
39// Exposes a local file through FileProvider as a content:// URI that other apps can read.
40QString QAndroidPlatformDrag::shareableUri(const QString &value)
41{
42 const QUrl url(value.trimmed());
43 if (!url.isLocalFile())
44 return value;
45 auto *iface = qApp->nativeInterface<QNativeInterface::QAndroidApplication>();
46 if (!iface)
47 return {};
48 const Context context = iface->context();
49 if (!context.isValid())
50 return {};
51
52 const QString authority = context.callMethod<QString>("getPackageName") + ".qtprovider"_L1;
53 const File file(url.toLocalFile());
54 const auto uri = FileProvider::callStaticMethod<Uri>("getUriForFile", context, authority, file);
55 if (!uri.isValid())
56 return {};
57
58 return uri.toString();
59}
60
61// See https://developer.android.com/reference/android/view/DragEvent
62namespace {
63enum AndroidDragAction : int {
64 ActionDragStarted = 1,
65 ActionDragLocation = 2,
66 ActionDrop = 3,
67 ActionDragEnded = 4,
68 ActionDragEntered = 5,
69 ActionDragExited = 6,
70};
71}
72
73QAndroidPlatformDrag::QAndroidPlatformDrag()
74{
75 s_currentDrag = this;
76 m_dragManager = QtDragManager::callStaticMethod<QtDragManager>("getInstance");
77 const jlong self = static_cast<jlong>(reinterpret_cast<quintptr>(this));
78 QtDragManager::callStaticMethod<void>("setNativePointer", self);
79}
80
81QAndroidPlatformDrag::~QAndroidPlatformDrag()
82{
83 // Clear only if still ours, so a freshly created integration is not zeroed.
84 if (s_currentDrag == this)
85 s_currentDrag = nullptr;
86 const jlong self = static_cast<jlong>(reinterpret_cast<quintptr>(this));
87 QtDragManager::callStaticMethod<void>("clearNativePointer", self);
88}
89
90bool QAndroidPlatformDrag::ownsDragObject() const
91{
92 return false;
93}
94
95Qt::DropAction QAndroidPlatformDrag::drag(QDrag *drag)
96{
97 // A nested drag would overwrite m_dragLoop and never let the outer one finish.
98 if (m_dragLoop) {
99 qCWarning(lcQpaDrag, "A drag is already in progress; nested drags are not supported.");
100 return Qt::IgnoreAction;
101 }
102
103 m_executedAction = Qt::IgnoreAction;
104
105 // Without the manager, startDrag() is a no-op and nothing would wake the drag loop.
106 if (!m_dragManager.isValid())
107 return Qt::IgnoreAction;
108
109 QWindow *window = QGuiApplication::focusWindow();
110 if (!window || !window->handle()) {
111 qCWarning(lcQpaDrag, "No focused window to start a drag.");
112 return Qt::IgnoreAction;
113 }
114
115 auto *platformWindow = static_cast<QAndroidPlatformWindow *>(window->handle());
116 QtWindow sourceView = platformWindow->nativeWindow();
117 if (!sourceView.isValid())
118 return Qt::IgnoreAction;
119
120 const QMimeData *mimeData = drag->mimeData();
121 QStringList mimeTypes;
122 QStringList clipData;
123 for (const QString &format : mimeData->formats()) {
124 QString value;
125 if (format == "text/uri-list"_L1) {
126 // Expose only shareable URIs and report any local file that cannot be shared.
127 QStringList uris;
128 for (const QUrl &url : mimeData->urls()) {
129 const QString uri = shareableUri(url.toString());
130 if (uri.isEmpty()) {
131 qCWarning(lcQpaDrag, "Cannot expose non-shareable %s to other apps.",
132 qPrintable(url.toString()));
133 continue;
134 }
135 uris.append(uri);
136 }
137 value = uris.join(u'\n');
138 } else if (format.startsWith("text/"_L1)) {
139 value = QString::fromUtf8(mimeData->data(format));
140 }
141 // Skip empty formats so a receiver sees no phantom types.
142 if (!value.isEmpty()) {
143 mimeTypes.append(format);
144 clipData.append(value);
145 }
146 }
147
148 // Drag shadow from the QDrag pixmap.
149 Bitmap shadowBitmap = QJniObject();
150 QPixmap pixmap = drag->pixmap();
151 if (pixmap.isNull() && drag->source()) {
152 // Android renders to a SurfaceView, so the platform drag shadow builder
153 // cannot capture window content. Grab the source as a pixmap instead.
154 const QMetaObject *metaObject = drag->source()->metaObject();
155 if (const int idx = metaObject->indexOfMethod("grab(QRect)"); idx >= 0) {
156 metaObject->method(idx).invoke(drag->source(), Qt::DirectConnection, qReturnArg(pixmap),
157 Q_ARG(QRect, QRect(QPoint(0, 0), QSize(-1, -1))));
158 }
159 }
160 QPoint hotSpot;
161 if (!pixmap.isNull()) {
162 QJniEnvironment env;
163 if (jobject bitmap = QtAndroid::createBitmap(pixmap.toImage(), env.jniEnv())) {
164 shadowBitmap = Bitmap(bitmap);
165 env->DeleteLocalRef(bitmap);
166 }
167 const qreal dpr = pixmap.devicePixelRatio();
168 if (const QPoint dragHotSpot = drag->hotSpot(); !dragHotSpot.isNull())
169 hotSpot = QPoint(qRound(dragHotSpot.x() * dpr), qRound(dragHotSpot.y() * dpr));
170 else
171 hotSpot = QPoint(pixmap.width() / 2, pixmap.height() / 2);
172 }
173 m_dragManager.callMethod<void>("startDrag", sourceView, mimeTypes, clipData,
174 shadowBitmap, hotSpot.x(), hotSpot.y());
175
176 QEventLoop loop;
177 m_dragLoop = &loop;
178 loop.exec();
179 m_dragLoop = nullptr;
180
181 return m_executedAction;
182}
183
184void QAndroidPlatformDrag::cancelDrag()
185{
186 if (m_dragManager.isValid())
187 m_dragManager.callMethod<void>("cancelDrag");
188 if (m_dragLoop)
189 m_dragLoop->quit();
190}
191
192bool QAndroidPlatformDrag::handleNativeDragEvent(jlong nativePointer, int viewId, int action,
193 float x, float y, const QStringList &mimeTypes,
194 const QStringList &clipData, bool result)
195{
196 auto onGuiThread = [nativePointer, viewId, action, x, y, mimeTypes, clipData, result] {
197 QAndroidPlatformDrag *drag = s_currentDrag;
198 if (!drag || reinterpret_cast<quintptr>(drag) != quintptr(nativePointer))
199 return false;
200 return drag->deliverDragEvent(viewId, action, x, y, mimeTypes, clipData, result);
201 };
202
203 // onDragEvent() runs on the Android UI thread and QWSI must deliver it on the Qt GUI thread.
204 if (QThread::currentThread() == qApp->thread())
205 return onGuiThread();
206
207 // ActionDragEnded needs no reply, so post it non-blocking.
208 if (action == ActionDragEnded) {
209 QMetaObject::invokeMethod(qApp, [onGuiThread] { onGuiThread(); }, Qt::QueuedConnection);
210 return true;
211 }
212
213 QtAndroidPrivate::AndroidDeadlockProtector protector(u"QAndroidPlatformDrag"_s);
214 if (!protector.acquire()) {
215 qCWarning(lcQpaDrag, "Skipping drag event delivery to avoid a UI/Qt thread deadlock.");
216 return false;
217 }
218
219 bool accepted = false;
220 QMetaObject::invokeMethod(qApp, [&] {
221 accepted = onGuiThread();
222 }, Qt::BlockingQueuedConnection);
223 return accepted;
224}
225
226bool QAndroidPlatformDrag::deliverDragEvent(int viewId, int action, float x, float y,
227 const QStringList &mimeTypes,
228 const QStringList &clipData,
229 bool result)
230{
231 // Quit the source's drag loop even if its window is gone, or drag() hangs.
232 if (action == ActionDragEnded) {
233 m_externalMimeData.reset();
234 // A cross-app drop reports only success, not the action, so assume the default.
235 if (result && m_executedAction == Qt::IgnoreAction) {
236 QDrag *drag = currentDrag();
237 m_executedAction = drag ? drag->defaultAction() : Qt::CopyAction;
238 }
239 if (m_dragLoop)
240 m_dragLoop->quit();
241 return true;
242 }
243
244 QWindow *window = QtAndroid::windowFromId(viewId);
245 if (!window) {
246 qCWarning(lcQpaDrag, "Cannot deliver drag event to null window with id %d.", viewId);
247 return false;
248 }
249
250 // A same-app drag still has its QDrag and live mime data. A cross-process
251 // drag has none, so reconstruct a QMimeData from the clip's mime types.
252 const QPoint pos(qRound(x), qRound(y));
253 const QMimeData *mimeData = nullptr;
254 Qt::DropActions supportedActions = Qt::CopyAction;
255 if (QDrag *drag = currentDrag()) {
256 mimeData = drag->mimeData();
257 supportedActions = drag->supportedActions();
258 m_externalMimeData.reset();
259 } else if (!mimeTypes.isEmpty()) {
260 if (!m_externalMimeData)
261 m_externalMimeData = std::make_unique<QMimeData>();
262 m_externalMimeData->clear();
263 for (qsizetype i = 0; i < mimeTypes.size(); ++i) {
264 const QString data = clipData.size() > i ? clipData.at(i) : QString();
265 m_externalMimeData->setData(mimeTypes.at(i), data.toUtf8());
266 }
267 mimeData = m_externalMimeData.get();
268 }
269
270 switch (action) {
271 case ActionDragLocation: {
272 const QPlatformDragQtResponse response = QWindowSystemInterface::handleDrag(
273 window, mimeData, pos, supportedActions, Qt::LeftButton, Qt::NoModifier);
274 return response.isAccepted();
275 }
276 case ActionDragExited: {
277 const QPlatformDragQtResponse response = QWindowSystemInterface::handleDrag(
278 window, nullptr, QPoint(), supportedActions, Qt::NoButton, Qt::NoModifier);
279 return response.isAccepted();
280 }
281 case ActionDrop: {
282 const QPlatformDropQtResponse response = QWindowSystemInterface::handleDrop(
283 window, mimeData, pos, supportedActions, Qt::LeftButton, Qt::NoModifier);
284 m_executedAction = response.isAccepted() ? response.acceptedAction() : Qt::IgnoreAction;
285 return response.isAccepted();
286 }
287 case ActionDragEntered:
288 default:
289 return true;
290 }
291}
292
293jboolean QAndroidPlatformDrag::onDragEvent(JNIEnv *env, jobject obj, jlong nativePointer,
294 jint viewId, jint action, jfloat x, jfloat y,
295 QJniArray<QString> mimeTypes,
296 QJniArray<QString> clipData, jboolean result)
297{
298 Q_UNUSED(env)
299 Q_UNUSED(obj)
300
301 if (!nativePointer)
302 return JNI_FALSE;
303
304 const QStringList types = mimeTypes.toContainer();
305 const QStringList data = clipData.toContainer();
306 if (!handleNativeDragEvent(nativePointer, viewId, action, x, y, types, data, result))
307 return JNI_FALSE;
308
309 return JNI_TRUE;
310}
311
312bool QAndroidPlatformDrag::registerNatives(QJniEnvironment &env)
313{
314 bool success = env.registerNativeMethods(
315 Traits<QtDragManager>::className(),
316 { Q_JNI_NATIVE_SCOPED_METHOD(onDragEvent, QAndroidPlatformDrag) });
317 if (!success) {
318 qCCritical(lcQpaDrag, "Failed to register QtDragManager native methods");
319 return false;
320 }
321 return true;
322}
323
324QT_END_NAMESPACE
325
326#endif // QT_CONFIG(draganddrop)