From 0c48e02bbd07a62ac534e7eb75888a328a80ce71 Mon Sep 17 00:00:00 2001 From: Axel Spoerl Date: Fri, 9 Dec 2022 13:45:11 +0100 Subject: Set geometry property in QXcbWindow after checking minimum size QXcbWindow::create() bound the window's size to windowMinimumSize(), after its size had been inherited from parent(). QPlatformWindow::setGeometry() was called before that sanity check. When a fullscreen window is re-mapped from a deactivated screen to the remaining screen, the call to QPlatformWindow::setGeometry() assigns an invalid QRect to QPlatformWindowPrivate::rect The negative int values x2 and/or y2 cause QXcbBackingStoreImage::flushPixmap to address unmapped memory and crash. This patch moves the call to QPlatformWindow::setGeometry() from before to after bounding to a minimum value. That assures a valid rectangle to be assigned in all cases. Fixes: QTBUG-109226 Change-Id: I349a0f3c721059a9013a275de5b4cb147fbdd7a1 Reviewed-by: Liang Qi (cherry picked from commit 6a3627b6c5aa5109a80024f3d7b0f938504f7ffe) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/xcb/qxcbwindow.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/xcb/qxcbwindow.cpp b/src/plugins/platforms/xcb/qxcbwindow.cpp index 21434d728e..ab3072250c 100644 --- a/src/plugins/platforms/xcb/qxcbwindow.cpp +++ b/src/plugins/platforms/xcb/qxcbwindow.cpp @@ -299,11 +299,6 @@ void QXcbWindow::create() return; } - QPlatformWindow::setGeometry(rect); - - if (platformScreen != currentScreen) - QWindowSystemInterface::handleWindowScreenChanged(window(), platformScreen->QPlatformScreen::screen()); - const QSize minimumSize = windowMinimumSize(); if (rect.width() > 0 || rect.height() > 0) { rect.setWidth(qBound(1, rect.width(), XCOORD_MAX)); @@ -315,6 +310,11 @@ void QXcbWindow::create() rect.setHeight(QHighDpi::toNativePixels(int(defaultWindowHeight), platformScreen->QPlatformScreen::screen())); } + QPlatformWindow::setGeometry(rect); + + if (platformScreen != currentScreen) + QWindowSystemInterface::handleWindowScreenChanged(window(), platformScreen->QPlatformScreen::screen()); + xcb_window_t xcb_parent_id = platformScreen->root(); if (parent()) { xcb_parent_id = static_cast(parent())->xcb_window(); -- cgit v1.2.3 From 680a48be9c8f13d5415791dad95876eddd3c3dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Thu, 15 Dec 2022 12:25:52 +0100 Subject: iOS a11y: Ensure parent elements are stacked below their children The accessibility system on iOS does not support elements that are both accessible themselves, and act as a container for other elements. You either return YES from isAccessibilityElement, which allows you to report accessible properties for the element, or NO, in which case you can implement the informal UIAccessibilityContainer protocol to report child elements. This was confirmed in Apple Q&A session on accessibility December 14. As Qt's accessibility system allow elements that are both containers and have properties of their own, we can't build a hierarchy of elements and containers, with only the leaf elements being accessible. Instead, we let each UIView act as a UIAccessibilityContainer, and report the entire child hierarchy as a single level of sibling accessible elements. In doing so, we include elements such as the Window or Dialog that root all the accessible elements. And apparently the order that we report these elements back to iOS determine the z-order of the elements, so we need to ensure "container" elements are behind their children. Otherwise assistive technologies such as Voice Control, or the Accessibility Inspector, will not be able to target the child elements. Fixes: QTBUG-108848 Change-Id: I5234bab2f14d5f368ae8c2672b051efcb80aa77d Reviewed-by: Timur Pocheptsov (cherry picked from commit 095604c9af90989957c974549bafdb0518c2ed3f) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/ios/quiview_accessibility.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/ios/quiview_accessibility.mm b/src/plugins/platforms/ios/quiview_accessibility.mm index 03849df0ee..91f7f9ea81 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -59,9 +59,11 @@ if (!iface) return; - [self createAccessibleElement: iface]; for (int i = 0; i < iface->childCount(); ++i) [self createAccessibleContainer: iface->child(i)]; + + // The container element must go last, so that it underlays all its children + [self createAccessibleElement:iface]; } - (void)initAccessibility -- cgit v1.2.3 From aa7b2436b5a357ea7d76eebaecab0c05719b5c91 Mon Sep 17 00:00:00 2001 From: Bartlomiej Moskal Date: Mon, 21 Nov 2022 17:28:52 +0100 Subject: Android: fix Android assets handler not listing dirs with only sub dirs It looks like AAssetDir_getNextFileName is not enough. Directories that contain only other directories (no files) were not listed. On the other hand, AAssetManager_openDir() will always return a pointer to initialized object (even if the specified directory does not exists), so we can't just leave only it here. Using FolderIterator as a last resort. This approach should not be too time consuming. As part of this fix, add some unit tests to cover/ensure assets listing/iterating works as expected. Fixes: QTBUG-107627 Change-Id: Id375fe8f99f4ca3f8cad4756f783ffafe5c074df Reviewed-by: Assam Boudjelthia (cherry picked from commit 4ceee3911a2ef567f614fc296475bc2b2a0e3add) --- .../platforms/android/qandroidassetsfileenginehandler.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 14fab3dce7..301eab8b10 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -140,6 +140,8 @@ public: FolderIterator(const QString &path) : m_path(path) { + // Note that empty dirs in the assets dir before the build are not going to be + // included in the final apk, so no empty folders should expected to be listed. QJNIObjectPrivate files = QJNIObjectPrivate::callStaticObjectMethod(QtAndroid::applicationClass(), "listAssetContent", "(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;", @@ -390,8 +392,13 @@ public: } else { auto *assetDir = AAssetManager_openDir(m_assetManager, m_fileName.toUtf8()); if (assetDir) { - if (AAssetDir_getNextFileName(assetDir)) + if (AAssetDir_getNextFileName(assetDir) + || (!FolderIterator::fromCache(m_fileName, false)->empty())) { + // If AAssetDir_getNextFileName is not valid, it still can be a directory that + // contains only other directories (no files). FolderIterator will not be called + // on the directory containing files so it should not be too time consuming now. m_assetInfo->type = AssetItem::Type::Folder; + } AAssetDir_close(assetDir); } } -- cgit v1.2.3 From 470587e7ca9a0c7886c820e2bf20fbded976b16f Mon Sep 17 00:00:00 2001 From: Samuel Mira Date: Wed, 21 Dec 2022 15:39:44 +0200 Subject: Android: Fix UI is scaled smaller than before The 413593183bbb1137fdc784d98c171d67a167bb32 patch changed the way how the display metrics are retrieved. By doing so, it was found that the previous way retrieved the scaledDensity always equal to density. It is intentional for scaledDensity to be dependent on the font scale chosen by the user. However, this change altered not only the font scale but also the layout. This patch will make the layout dependent on the density instead of the scaledDensity and normalize the way the display metrics are retrieved among Android versions. Currently, the fontScale is ignored, QTBUG-109566 will track future developments. Fixes: QTBUG-109026 Change-Id: I6adacd17583cbe9bee368af35c50b780872ab222 Reviewed-by: Ville Voutilainen (cherry picked from commit 99893a914a821567e10935ffb8be24df7147ccd9) Reviewed-by: Qt CI Bot --- src/plugins/platforms/android/qandroidplatformscreen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/qandroidplatformscreen.cpp b/src/plugins/platforms/android/qandroidplatformscreen.cpp index fd184e6757..410f95ac58 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.cpp +++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp @@ -497,7 +497,7 @@ static const int androidLogicalDpi = 72; QDpi QAndroidPlatformScreen::logicalDpi() const { - qreal lDpi = QtAndroid::scaledDensity() * androidLogicalDpi; + qreal lDpi = QtAndroid::pixelDensity() * androidLogicalDpi; return QDpi(lDpi, lDpi); } -- cgit v1.2.3 From 460e60e2d1631cb2d480acdd3382ed6340cdc7d3 Mon Sep 17 00:00:00 2001 From: Liang Qi Date: Wed, 14 Dec 2022 15:52:03 +0100 Subject: ibus: add SetCursorLocationRelative in InputContext.xml Task-number: QTBUG-103393 Change-Id: I90c48a0d698636ed289d6f6c1485875e2e91fb34 Reviewed-by: Ilya Fedin Reviewed-by: Weng Xuetian Reviewed-by: Liang Qi (cherry picked from commit 54002671bd68b1c59b61a630c9333b2aab286483) Reviewed-by: Qt Cherry-pick Bot --- .../ibus/interfaces/org.freedesktop.IBus.InputContext.xml | 6 ++++++ src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h | 7 +++++++ 2 files changed, 13 insertions(+) (limited to 'src/plugins') diff --git a/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml b/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml index 9c67a38c57..30c326d06f 100644 --- a/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml +++ b/src/plugins/platforminputcontexts/ibus/interfaces/org.freedesktop.IBus.InputContext.xml @@ -14,6 +14,12 @@ + + + + + + diff --git a/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h b/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h index 396a213aaa..31d5a71c41 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h +++ b/src/plugins/platforminputcontexts/ibus/qibusinputcontextproxy.h @@ -112,6 +112,13 @@ public Q_SLOTS: // METHODS return asyncCallWithArgumentList(QLatin1String("SetCursorLocation"), argumentList); } + inline QDBusPendingReply<> SetCursorLocationRelative(int x, int y, int w, int h) + { + QList argumentList; + argumentList << QVariant::fromValue(x) << QVariant::fromValue(y) << QVariant::fromValue(w) << QVariant::fromValue(h); + return asyncCallWithArgumentList(QLatin1String("SetCursorLocationRelative"), argumentList); + } + inline QDBusPendingReply<> SetEngine(const QString &name) { QList argumentList; -- cgit v1.2.3 From 0da9993a3ca056db9c0c5b3aa636e3b5f4aef9df Mon Sep 17 00:00:00 2001 From: Liang Qi Date: Tue, 6 Dec 2022 13:01:29 +0100 Subject: ibus: support high dpi for cursor rectangle on both X11/xcb and Wayland. Following similar approach in QFcitxPlatformInputContext::cursorRectChanged() https://github.com/fcitx/fcitx5-qt/blob/master/qt5/platforminputcontext/qfcitxplatforminputcontext.cpp#L490-L532 Tested with following configurations: * GNOME on xorg, 100%/125%/150%/200% scale, 1 and 2 monitors * KDE/Plasma X11, 100%/150%/200% scale, 1 monitor * GNOME on Wayland, 100%/200% scale, 1 and 2 monitors Enable fractional scale on GNOME: gsettings set org.gnome.mutter experimental-features "['scale-monitor-framebuffer']" Fixes: QTBUG-103393 Change-Id: Idfd3153e4cd9f9530b4db6f089830ec47451a19e Reviewed-by: Ilya Fedin Reviewed-by: Weng Xuetian Reviewed-by: Liang Qi (cherry picked from commit 3790821b220ff6ab3c51a5c0b35581c00bc4e84d) --- .../ibus/qibusplatforminputcontext.cpp | 27 +++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp index d980b8b9f6..f39737785c 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp +++ b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp @@ -254,10 +254,31 @@ void QIBusPlatformInputContext::cursorRectChanged() QWindow *inputWindow = qApp->focusWindow(); if (!inputWindow) return; - r.moveTopLeft(inputWindow->mapToGlobal(r.topLeft())); + if (!inputWindow->screen()) + return; + + if (QGuiApplication::platformName().startsWith("wayland")) { + auto margins = inputWindow->frameMargins(); + r.translate(margins.left(), margins.top()); + qreal scale = inputWindow->devicePixelRatio(); + QRect newRect = QRect(r.x() * scale, r.y() * scale, r.width() * scale, r.height() * scale); + if (debug) + qDebug() << "microFocus" << newRect; + d->context->SetCursorLocationRelative(newRect.x(), newRect.y(), + newRect.width(), newRect.height()); + return; + } + + // x11/xcb + auto screenGeometry = inputWindow->screen()->geometry(); + auto point = inputWindow->mapToGlobal(r.topLeft()); + qreal scale = inputWindow->devicePixelRatio(); + auto native = (point - screenGeometry.topLeft()) * scale + screenGeometry.topLeft(); + QRect newRect(native, r.size() * scale); if (debug) - qDebug() << "microFocus" << r; - d->context->SetCursorLocation(r.x(), r.y(), r.width(), r.height()); + qDebug() << "microFocus" << newRect; + d->context->SetCursorLocation(newRect.x(), newRect.y(), + newRect.width(), newRect.height()); } void QIBusPlatformInputContext::setFocusObject(QObject *object) -- cgit v1.2.3 From 5aca4e09ec22d4219df168207e08ce5b104a51cb Mon Sep 17 00:00:00 2001 From: Julian Greilich Date: Wed, 4 Jan 2023 16:32:28 +0100 Subject: Android A11Y: Only access the main thread when it is not blocked When the qtMainLoopThread calls QSGThreadedRenderLoop::polishAndSync(), it waits for the QSGRenderThread. In the QSGRenderThread, QAndroidPlatformOpenGLWindow::eglSurface() calls QtAndroid::createSurface() and waits for the "android main thread" to return a valid surface. When the "android main thread" now calls "runInObjectContext" (e.g. by calling QtAndroidAccessibility::childIdListForAccessibleObject()) it waits for the qtMainLoopThread and the program is stuck in a deadlock. To prevent this, we protect all BlockedQueuedConnection from the "android main thread" to the qtMainLoopThread by acquiring the AndroidDeadlockProtector. When QAndroidPlatformOpenGLWindow::eglSurface() already acquired the AndroidDeadlockProtector we abort the current A11y call with an emtpy or default value. Note: b8a95275440b8a143ee648466fd8b5401ee1e839 already tried to fix this by checking "getSurfaceCount() != 0", but there are situations, where a new surface is being created while an old surface is still present. Task-number: QTBUG-105958 Change-Id: Ie40e8654c99aace9e69b0b8412952fa22c89f071 Reviewed-by: Assam Boudjelthia (cherry picked from commit b832a5ac72c6015b6509d60b75b2ce5d5e570800) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/android/androidjniaccessibility.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp index 78517e845e..c774a85009 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.cpp +++ b/src/plugins/platforms/android/androidjniaccessibility.cpp @@ -37,6 +37,7 @@ ** ****************************************************************************/ +#include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" #include "androidjnimain.h" #include "qandroidplatformintegration.h" @@ -96,6 +97,14 @@ namespace QtAndroidAccessibility template void runInObjectContext(QObject *context, Func &&func, Ret *retVal) { + AndroidDeadlockProtector protector; + if (!protector.acquire()) { + __android_log_print(ANDROID_LOG_WARN, m_qtTag, + "Could not run accessibility call in object context, accessing " + "main thread could lead to deadlock"); + return; + } + if (!QtAndroid::blockEventLoopsWhenSuspended() || QGuiApplication::applicationState() != Qt::ApplicationSuspended) { QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal); -- cgit v1.2.3 From cf83bd0d50029b543bc5b47cb15dce047e0ce0cf Mon Sep 17 00:00:00 2001 From: Jannis Xiong Date: Fri, 16 Sep 2022 11:29:49 +0800 Subject: Fix cache maybe invalid while the signal is actived from queue with default QObject::connect signal may active from next message loop. invalide cache will hit while accessibility interface is called from windows. Invalide cache will lead to a crash Fixes: QTBUG-106653 Change-Id: I5359672bcd60ed6cfb2edf238645225164cb1b88 Reviewed-by: Oliver Wolff (cherry picked from commit 80f44954f6872afb5aa37e6737c3e1ac68ad3577) Reviewed-by: Qt Cherry-pick Bot --- .../platforms/windows/uiautomation/qwindowsuiamainprovider.cpp | 5 +++++ src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h | 2 ++ .../platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp index c48c2ee8f5..103b43591f 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp @@ -74,10 +74,13 @@ QT_BEGIN_NAMESPACE using namespace QWindowsUiAutomation; +QMutex QWindowsUiaMainProvider::m_mutex; // Returns a cached instance of the provider for a specific acessible interface. QWindowsUiaMainProvider *QWindowsUiaMainProvider::providerForAccessible(QAccessibleInterface *accessible) { + QMutexLocker locker(&m_mutex); + if (!accessible) return nullptr; @@ -271,6 +274,8 @@ ULONG QWindowsUiaMainProvider::AddRef() ULONG STDMETHODCALLTYPE QWindowsUiaMainProvider::Release() { + QMutexLocker locker(&m_mutex); + if (!--m_ref) { delete this; return 0; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h index edbf9bcf35..6fc80e8356 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h @@ -47,6 +47,7 @@ #include #include +#include #include #include @@ -98,6 +99,7 @@ public: private: QString automationIdForAccessible(const QAccessibleInterface *accessible); ULONG m_ref; + static QMutex m_mutex; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp index c4fee0286f..75f406b891 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp @@ -75,7 +75,7 @@ void QWindowsUiaProviderCache::insert(QAccessible::Id id, QWindowsUiaBaseProvide m_providerTable[id] = provider; m_inverseTable[provider] = id; // Connects the destroyed signal to our slot, to remove deleted objects from the cache. - QObject::connect(provider, &QObject::destroyed, this, &QWindowsUiaProviderCache::objectDestroyed); + QObject::connect(provider, &QObject::destroyed, this, &QWindowsUiaProviderCache::objectDestroyed, Qt::DirectConnection); } } -- cgit v1.2.3 From 762adef2a9c4f320d31a6739ecf05878a4a79e4c Mon Sep 17 00:00:00 2001 From: Assam Boudjelthia Date: Tue, 22 Nov 2022 14:02:11 +0200 Subject: Fix infinite loop when iterating content uri sub-files/dirs make QAbstractFileEngineIterator::currentFilePath() virtual and implement it under AndroidContentFileEngine to return current fileName because content uris shouldn't be constructed manaully like normal file paths. Fixes: QTBUG-104776 Change-Id: I4643a73a3bd4019bedaa056c35468117bcec18dc Reviewed-by: Ville Voutilainen (cherry picked from commit f3c998510d3a6c8fc468e449d66b0280119d0a8f) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/android/androidcontentfileengine.cpp | 5 +++++ src/plugins/platforms/android/androidcontentfileengine.h | 1 + src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 3b20682a69..f273d27d28 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -213,3 +213,8 @@ QString AndroidContentFileEngineIterator::currentFileName() const return QString(); return m_entries.at(m_index - 1); } + +QString AndroidContentFileEngineIterator::currentFilePath() const +{ + return currentFileName(); +} diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index 153c99d2f0..f92e437fab 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -73,6 +73,7 @@ public: QString next() override; bool hasNext() const override; QString currentFileName() const override; + QString currentFilePath() const override; private: mutable QStringList m_entries; mutable int m_index = -1; diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 301eab8b10..fe3fb1d2cf 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -220,7 +220,7 @@ public: return m_currentIterator->currentFileName(); } - virtual QString currentFilePath() const + QString currentFilePath() const override { if (!m_currentIterator) return {}; -- cgit v1.2.3 From 01679de6e6caf5c2d9dc3f3a85a8474d95461dbd Mon Sep 17 00:00:00 2001 From: Assam Boudjelthia Date: Wed, 23 Nov 2022 14:30:50 +0200 Subject: Android: Add facilities to handle more content URIs operations Use DocumentFile and DocumentsContract to support more operations on content URIs, such as: * listing files and subdirectories with usable content uris * mkdir, rmdir * creating non-existing files under a tree uri * remove And since dealing with content URIs require some level of user interation, manual tests were added to cover what's been implemented. Note: parts of the code were from from BogDan Vatra . Task-number: QTBUG-98974 Task-number: QTBUG-104776 Change-Id: I3d64958ef26d0155210905b65daae2efa3db31c1 Reviewed-by: Ville Voutilainen (cherry picked from commit e5d591a0d09032d1870e47d1bf59c9069ea0a943) --- .../platforms/android/androidcontentfileengine.cpp | 725 +++++++++++++++++++-- .../platforms/android/androidcontentfileengine.h | 63 +- 2 files changed, 716 insertions(+), 72 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index f273d27d28..a1027229ee 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -1,6 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2019 Volker Krause +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. @@ -39,15 +40,51 @@ #include "androidcontentfileengine.h" -#include #include -#include +#include +#include +#include +#include +#include -AndroidContentFileEngine::AndroidContentFileEngine(const QString &f) - : m_file(f) +class JniExceptionCleaner { - setFileName(f); +public: + JniExceptionCleaner() { clearException(); }; + ~JniExceptionCleaner() { clearException(); } + + bool clean() { return clearException(); }; +private: + bool clearException() + { + QJNIEnvironmentPrivate env; + if (Q_UNLIKELY(env->ExceptionCheck())) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return true; + } + return false; + } +}; + +static QJNIObjectPrivate &contentResolverInstance() +{ + static QJNIObjectPrivate contentResolver; + if (!contentResolver.isValid()) { + JniExceptionCleaner cleaner; + contentResolver = QJNIObjectPrivate(QtAndroidPrivate::context()) + .callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); + } + + return contentResolver; +} + +AndroidContentFileEngine::AndroidContentFileEngine(const QString &filename) + : m_initialFile(filename), + m_documentFile(DocumentFile::parseFromAnyUri(filename)) +{ + setFileName(filename); } bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) @@ -58,6 +95,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) } if (openMode & QFileDevice::WriteOnly) { openModeStr += QLatin1Char('w'); + if (!m_documentFile->exists()) { + if (QUrl(m_initialFile).path().startsWith(QLatin1String("/tree/"))) { + const int lastSeparatorIndex = m_initialFile.lastIndexOf(QLatin1Char('/')); + const QString fileName = m_initialFile.mid(lastSeparatorIndex + 1); + + QString mimeType; + const auto mimeTypes = QMimeDatabase().mimeTypesForFileName(fileName); + if (!mimeTypes.empty()) + mimeType = mimeTypes.first().name(); + else + mimeType = QLatin1String("application/octet-stream"); + + if (m_documentFile->parent()) { + auto createdFile = m_documentFile->parent()->createFile(mimeType, fileName); + if (createdFile) + m_documentFile = createdFile; + } + } else { + qWarning() << "open(): non-existent content URI with a document type provided"; + } + } } if (openMode & QFileDevice::Truncate) { openModeStr += QLatin1Char('t'); @@ -65,53 +123,140 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) openModeStr += QLatin1Char('a'); } - const auto fd = QJNIObjectPrivate::callStaticMethod("org/qtproject/qt5/android/QtNative", - "openFdForContentUrl", - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)I", - QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object(), - QJNIObjectPrivate::fromString(openModeStr).object()); + JniExceptionCleaner cleaner; + m_pfd = contentResolverInstance().callObjectMethod("openFileDescriptor", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", + m_documentFile->uri().object(), + QJNIObjectPrivate::fromString(openModeStr).object()); + + if (!m_pfd.isValid()) + return false; + + const auto fd = m_pfd.callMethod("getFd", "()I"); if (fd < 0) { + closeNativeFileDescriptor(); return false; } - return QFSFileEngine::open(openMode, fd, QFile::AutoCloseHandle); + return QFSFileEngine::open(openMode, fd, QFile::DontCloseHandle); +} + +bool AndroidContentFileEngine::close() +{ + closeNativeFileDescriptor(); + return QFSFileEngine::close(); +} + +void AndroidContentFileEngine::closeNativeFileDescriptor() +{ + if (m_pfd.isValid()) { + JniExceptionCleaner cleaner; + m_pfd.callMethod("close", "()V"); + m_pfd = QJNIObjectPrivate(); + } } qint64 AndroidContentFileEngine::size() const { - const jlong size = QJNIObjectPrivate::callStaticMethod( - "org/qtproject/qt5/android/QtNative", "getSize", - "(Landroid/content/Context;Ljava/lang/String;)J", QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object()); - return (qint64)size; + return m_documentFile->length(); +} + +bool AndroidContentFileEngine::remove() +{ + return m_documentFile->remove(); +} + +bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories) const +{ + QString tmp = dirName; + tmp.remove(m_initialFile); + + QStringList dirParts = tmp.split(QLatin1Char('/')); + dirParts.removeAll(""); + + if (dirParts.isEmpty()) + return false; + + auto createdDir = m_documentFile; + bool allDirsCreated = true; + for (const auto &dir : dirParts) { + // Find if the sub-dir already exists and then don't re-create it + bool subDirExists = false; + for (const DocumentFilePtr &subDir : m_documentFile->listFiles()) { + if (dir == subDir->name() && subDir->isDirectory()) { + createdDir = subDir; + subDirExists = true; + } + } + + if (!subDirExists) { + createdDir = createdDir->createDirectory(dir); + if (!createdDir) { + allDirsCreated = false; + break; + } + } + + if (!createParentDirectories) + break; + } + + return allDirsCreated; +} + +bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const +{ + if (recurseParentDirectories) + qWarning() << "rmpath(): Unsupported for Content URIs"; + + const QString dirFileName = QUrl(dirName).fileName(); + bool deleted = false; + for (const DocumentFilePtr &dir : m_documentFile->listFiles()) { + if (dirFileName == dir->name() && dir->isDirectory()) { + deleted = dir->remove(); + break; + } + } + + return deleted; +} + +QByteArray AndroidContentFileEngine::id() const +{ + return m_documentFile->id().toUtf8(); +} + +QDateTime AndroidContentFileEngine::fileTime(FileTime time) const +{ + switch (time) { + case FileTime::ModificationTime: + return m_documentFile->lastModified(); + break; + default: + break; + } + + return QDateTime(); } AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlags type) const { - FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag); FileFlags flags; - const bool isDir = QJNIObjectPrivate::callStaticMethod( - "org/qtproject/qt5/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object()); - // If it is a directory then we know it exists so there is no reason to explicitly check - const bool exists = isDir ? true : QJNIObjectPrivate::callStaticMethod( - "org/qtproject/qt5/android/QtNative", "checkFileExists", - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object()); - if (!exists && !isDir) + if (!m_documentFile->exists()) return flags; - if (isDir) { - flags = DirectoryType | commonFlags; + + flags = ExistsFlag; + if (!m_documentFile->canRead()) + return flags; + + flags |= ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm; + + if (m_documentFile->isDirectory()) { + flags |= DirectoryType; } else { - flags = FileType | commonFlags; - const bool writable = QJNIObjectPrivate::callStaticMethod( - "org/qtproject/qt5/android/QtNative", "checkIfWritable", - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object()); - if (writable) + flags |= FileType; + if (m_documentFile->canWrite()) flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm; } return type & flags; @@ -126,18 +271,18 @@ QString AndroidContentFileEngine::fileName(FileName f) const case DefaultName: case AbsoluteName: case CanonicalName: - return m_file; + return m_documentFile->uri().toString(); case BaseName: - { - const int pos = m_file.lastIndexOf(QChar(QLatin1Char('/'))); - return m_file.mid(pos); - } + return m_documentFile->name(); default: - return QString(); + break; } + + return QString(); } -QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, const QStringList &filterNames) +QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, + const QStringList &filterNames) { return new AndroidContentFileEngineIterator(filters, filterNames); } @@ -179,42 +324,484 @@ QString AndroidContentFileEngineIterator::next() bool AndroidContentFileEngineIterator::hasNext() const { - if (m_index == -1) { - if (path().isEmpty()) + if (m_index == -1 && m_files.isEmpty()) { + const auto currentPath = path(); + if (currentPath.isEmpty()) return false; - const bool isDir = QJNIObjectPrivate::callStaticMethod( - "org/qtproject/qt5/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", - QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(path()).object()); - if (isDir) { - QJNIObjectPrivate objArray = QJNIObjectPrivate::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", - "listContentsFromTreeUri", - "(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;", - QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(path()).object()); - if (objArray.isValid()) { - QJNIEnvironmentPrivate env; - const jsize length = env->GetArrayLength(static_cast(objArray.object())); - for (int i = 0; i != length; ++i) { - m_entries << QJNIObjectPrivate(env->GetObjectArrayElement( - static_cast(objArray.object()), i)).toString(); - } - } - } - m_index = 0; + + const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath); + if (iterDoc->isDirectory()) + for (const auto &doc : iterDoc->listFiles()) + m_files.append(doc); } - return m_index < m_entries.size(); + + return m_index < (m_files.size() - 1); } QString AndroidContentFileEngineIterator::currentFileName() const { - if (m_index <= 0 || m_index > m_entries.size()) + if (m_index < 0 || m_index > m_files.size()) return QString(); - return m_entries.at(m_index - 1); + // Returns a full path since contstructing a content path from the file name + // and a tree URI only will not point to a valid file URI. + return m_files.at(m_index)->uri().toString(); } QString AndroidContentFileEngineIterator::currentFilePath() const { return currentFileName(); } + +// Start of Cursor + +class Cursor +{ +public: + explicit Cursor(const QJNIObjectPrivate &object) + : m_object{object} { } + + ~Cursor() + { + if (m_object.isValid()) { + JniExceptionCleaner cleaner; + m_object.callMethod("close"); + } + } + + enum Type { + FIELD_TYPE_NULL = 0x00000000, + FIELD_TYPE_INTEGER = 0x00000001, + FIELD_TYPE_FLOAT = 0x00000002, + FIELD_TYPE_STRING = 0x00000003, + FIELD_TYPE_BLOB = 0x00000004 + }; + + QVariant data(int columnIndex) const + { + JniExceptionCleaner cleaner; + int type = m_object.callMethod("getType", "(I)I", columnIndex); + switch (type) { + case FIELD_TYPE_NULL: + return {}; + case FIELD_TYPE_INTEGER: + return QVariant::fromValue(m_object.callMethod("getLong", "(I)J", columnIndex)); + case FIELD_TYPE_FLOAT: + return QVariant::fromValue(m_object.callMethod("getDouble", "(I)D", + columnIndex)); + case FIELD_TYPE_STRING: + return QVariant::fromValue(m_object.callObjectMethod("getString", + "(I)Ljava/lang/String;", + columnIndex).toString()); + case FIELD_TYPE_BLOB: { + auto blob = m_object.callObjectMethod("getBlob", "(I)[B", columnIndex); + QJNIEnvironmentPrivate env; + const auto blobArray = static_cast(blob.object()); + const int size = env->GetArrayLength(blobArray); + const auto byteArray = env->GetByteArrayElements(blobArray, nullptr); + QByteArray data{reinterpret_cast(byteArray), size}; + env->ReleaseByteArrayElements(blobArray, byteArray, 0); + return QVariant::fromValue(data); + } + } + return {}; + } + + static std::unique_ptr queryUri(const QJNIObjectPrivate &uri, + const QStringList &projection = {}, + const QString &selection = {}, + const QStringList &selectionArgs = {}, + const QString &sortOrder = {}) + { + JniExceptionCleaner cleaner; + auto cursor = contentResolverInstance().callObjectMethod("query", + "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;", + uri.object(), + projection.isEmpty() ? nullptr : fromStringList(projection).object(), + selection.isEmpty() ? nullptr : QJNIObjectPrivate::fromString(selection).object(), + selectionArgs.isEmpty() ? nullptr : fromStringList(selectionArgs).object(), + sortOrder.isEmpty() ? nullptr : QJNIObjectPrivate::fromString(sortOrder).object()); + if (!cursor.isValid()) + return {}; + return std::make_unique(cursor); + } + + static QVariant queryColumn(const QJNIObjectPrivate &uri, const QString &column) + { + const auto query = queryUri(uri, {column}); + if (!query) + return {}; + + if (query->rowCount() != 1 || query->columnCount() != 1) + return {}; + query->moveToFirst(); + return query->data(0); + } + + bool isNull(int columnIndex) const + { + return m_object.callMethod("isNull", "(I)Z", columnIndex); + } + + int columnCount() const { return m_object.callMethod("getColumnCount"); } + int rowCount() const { return m_object.callMethod("getCount"); } + int row() const { return m_object.callMethod("getPosition"); } + bool isFirst() const { return m_object.callMethod("isFirst"); } + bool isLast() const { return m_object.callMethod("isLast"); } + bool moveToFirst() { return m_object.callMethod("moveToFirst"); } + bool moveToLast() { return m_object.callMethod("moveToLast"); } + bool moveToNext() { return m_object.callMethod("moveToNext"); } + +private: + static QJNIObjectPrivate fromStringList(const QStringList &list) + { + QJNIEnvironmentPrivate env; + JniExceptionCleaner cleaner; + auto array = env->NewObjectArray(list.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < list.size(); ++i) + env->SetObjectArrayElement(array, i, QJNIObjectPrivate::fromString(list[i]).object()); + return QJNIObjectPrivate::fromLocalRef(array); + } + + QJNIObjectPrivate m_object; +}; + +// End of Cursor + +// Start of DocumentsContract + +/*! + * + * DocumentsContract Api. + * Check https://developer.android.com/reference/android/provider/DocumentsContract + * for more information. + * + * \note This does not implement all facilities of the native API. + * + */ +namespace DocumentsContract +{ + +namespace Document { +const QLatin1String COLUMN_DISPLAY_NAME("_display_name"); +const QLatin1String COLUMN_DOCUMENT_ID("document_id"); +const QLatin1String COLUMN_FLAGS("flags"); +const QLatin1String COLUMN_LAST_MODIFIED("last_modified"); +const QLatin1String COLUMN_MIME_TYPE("mime_type"); +const QLatin1String COLUMN_SIZE("_size"); + +constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008; +constexpr int FLAG_SUPPORTS_DELETE = 0x00000004; +constexpr int FLAG_SUPPORTS_WRITE = 0x00000002; +constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200; + +const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory"); +} // namespace Document + +QString documentId(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "getDocumentId", + "(Landroid/net/Uri;)Ljava/lang/String;", + uri.object()).toString(); +} + +QString treeDocumentId(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "getTreeDocumentId", + "(Landroid/net/Uri;)Ljava/lang/String;", + uri.object()).toString(); +} + +QJNIObjectPrivate buildChildDocumentsUriUsingTree(const QJNIObjectPrivate &uri, const QString &parentDocumentId) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "buildChildDocumentsUriUsingTree", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", + uri.object(), + QJNIObjectPrivate::fromString(parentDocumentId).object()); + +} + +QJNIObjectPrivate buildDocumentUriUsingTree(const QJNIObjectPrivate &treeUri, const QString &documentId) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "buildDocumentUriUsingTree", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", + treeUri.object(), + QJNIObjectPrivate::fromString(documentId).object()); +} + +bool isDocumentUri(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticMethod("android/provider/DocumentsContract", + "isDocumentUri", + "(Landroid/content/Context;Landroid/net/Uri;)Z", + QtAndroidPrivate::context(), + uri.object()); +} + +bool isTreeUri(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticMethod("android/provider/DocumentsContract", + "isTreeUri", + "(Landroid/net/Uri;)Z", + uri.object()); +} + +QJNIObjectPrivate createDocument(const QJNIObjectPrivate &parentDocumentUri, const QString &mimeType, + const QString &displayName) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "createDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;", + contentResolverInstance().object(), + parentDocumentUri.object(), + QJNIObjectPrivate::fromString(mimeType).object(), + QJNIObjectPrivate::fromString(displayName).object()); +} + +bool deleteDocument(const QJNIObjectPrivate &documentUri) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_DELETE)) + return {}; + + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticMethod("android/provider/DocumentsContract", + "deleteDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;)Z", + contentResolverInstance().object(), + documentUri.object()); +} + +} // End DocumentsContract namespace + +// Start of DocumentFile + +using namespace DocumentsContract; + +namespace { +class MakeableDocumentFile : public DocumentFile +{ +public: + MakeableDocumentFile(const QJNIObjectPrivate &uri, const DocumentFilePtr &parent = {}) + : DocumentFile(uri, parent) + {} +}; +} + +DocumentFile::DocumentFile(const QJNIObjectPrivate &uri, + const DocumentFilePtr &parent) + : m_uri{uri} + , m_parent{parent} +{} + +QJNIObjectPrivate parseUri(const QString &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/net/Uri", + "parse", + "(Ljava/lang/String;)Landroid/net/Uri;", + QJNIObjectPrivate::fromString(uri).object()); +} + +DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName) +{ + const QJNIObjectPrivate uri = parseUri(fileName); + + if (DocumentsContract::isDocumentUri(uri)) + return fromSingleUri(uri); + + const QString documentType = QLatin1String("/document/"); + const QString treeType = QLatin1String("/tree/"); + + const int treeIndex = fileName.indexOf(treeType); + const int documentIndex = fileName.indexOf(documentType); + const int index = fileName.lastIndexOf(QLatin1Char('/')); + + if (index <= treeIndex + treeType.size() || index <= documentIndex + documentType.size()) + return fromTreeUri(uri); + + const QString parentUrl = fileName.left(index); + DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl)); + + const QString baseName = fileName.mid(index); + const QString fileUrl = parentUrl + QUrl::toPercentEncoding(baseName); + + DocumentFilePtr docFile = std::make_shared(parseUri(fileUrl)); + if (parentDocFile && parentDocFile->isDirectory()) + docFile->m_parent = parentDocFile; + + return docFile; +} + +DocumentFilePtr DocumentFile::fromSingleUri(const QJNIObjectPrivate &uri) +{ + return std::make_shared(uri); +} + +DocumentFilePtr DocumentFile::fromTreeUri(const QJNIObjectPrivate &treeUri) +{ + QString docId; + if (isDocumentUri(treeUri)) + docId = documentId(treeUri); + else + docId = treeDocumentId(treeUri); + + return std::make_shared(buildDocumentUriUsingTree(treeUri, docId)); +} + +DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName) +{ + if (isDirectory()) { + return std::make_shared( + createDocument(m_uri, mimeType, displayName), + shared_from_this()); + } + return {}; +} + +DocumentFilePtr DocumentFile::createDirectory(const QString &displayName) +{ + if (isDirectory()) { + return std::make_shared( + createDocument(m_uri, Document::MIME_TYPE_DIR, displayName), + shared_from_this()); + } + return {}; +} + +const QJNIObjectPrivate &DocumentFile::uri() const +{ + return m_uri; +} + +const DocumentFilePtr &DocumentFile::parent() const +{ + return m_parent; +} + +QString DocumentFile::name() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString(); +} + +QString DocumentFile::id() const +{ + return DocumentsContract::documentId(uri()); +} + +QString DocumentFile::mimeType() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_MIME_TYPE).toString(); +} + +bool DocumentFile::isDirectory() const +{ + return mimeType() == Document::MIME_TYPE_DIR; +} + +bool DocumentFile::isFile() const +{ + const QString type = mimeType(); + return type != Document::MIME_TYPE_DIR && !type.isEmpty(); +} + +bool DocumentFile::isVirtual() const +{ + return isDocumentUri(m_uri) && (Cursor::queryColumn(m_uri, + Document::COLUMN_FLAGS).toInt() & Document::FLAG_VIRTUAL_DOCUMENT); +} + +QDateTime DocumentFile::lastModified() const +{ + const auto timeVariant = Cursor::queryColumn(m_uri, Document::COLUMN_LAST_MODIFIED); + if (timeVariant.isValid()) + return QDateTime::fromMSecsSinceEpoch(timeVariant.toLongLong()); + return {}; +} + +int64_t DocumentFile::length() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_SIZE).toLongLong(); +} + +namespace { +constexpr int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001; +constexpr int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002; +} + +bool DocumentFile::canRead() const +{ + JniExceptionCleaner cleaner; + const auto context = QJNIObjectPrivate(QtAndroidPrivate::context()); + const bool selfUriPermission = context.callMethod("checkCallingOrSelfUriPermission", + "(Landroid/net/Uri;I)I", + m_uri.object(), + FLAG_GRANT_READ_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + return !mimeType().isEmpty(); +} + +bool DocumentFile::canWrite() const +{ + JniExceptionCleaner cleaner; + const auto context = QJNIObjectPrivate(QtAndroidPrivate::context()); + const bool selfUriPermission = context.callMethod("checkCallingOrSelfUriPermission", + "(Landroid/net/Uri;I)I", + m_uri.object(), + FLAG_GRANT_WRITE_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + const QString type = mimeType(); + if (type.isEmpty()) + return false; + + const int flags = Cursor::queryColumn(m_uri, Document::COLUMN_FLAGS).toInt(); + if (flags & Document::FLAG_SUPPORTS_DELETE) + return true; + + const bool supportsWrite = (flags & Document::FLAG_SUPPORTS_WRITE); + const bool isDir = (type == Document::MIME_TYPE_DIR); + const bool dirSupportsCreate = (isDir && (flags & Document::FLAG_DIR_SUPPORTS_CREATE)); + + return dirSupportsCreate || supportsWrite; +} + +bool DocumentFile::remove() +{ + return deleteDocument(m_uri); +} + +bool DocumentFile::exists() const +{ + return !name().isEmpty(); +} + +std::vector DocumentFile::listFiles() +{ + std::vector res; + const auto childrenUri = buildChildDocumentsUriUsingTree(m_uri, documentId(m_uri)); + const auto query = Cursor::queryUri(childrenUri, {Document::COLUMN_DOCUMENT_ID}); + if (!query) + return res; + + while (query->moveToNext()) { + const auto uri = buildDocumentUriUsingTree(m_uri, query->data(0).toString()); + res.push_back(std::make_shared(uri, shared_from_this())); + } + return res; +} + +// End of DocumentFile diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index f92e437fab..e6457bc56d 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -1,6 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2019 Volker Krause +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. @@ -41,20 +42,36 @@ #define ANDROIDCONTENTFILEENGINE_H #include +#include +#include + + +using DocumentFilePtr = std::shared_ptr; class AndroidContentFileEngine : public QFSFileEngine { public: AndroidContentFileEngine(const QString &fileName); bool open(QIODevice::OpenMode openMode) override; + bool close() override; qint64 size() const override; + bool remove() override; + bool mkdir(const QString &dirName, bool createParentDirectories) const override; + bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; + QByteArray id() const override; + bool caseSensitive() const override { return true; } + QDateTime fileTime(FileTime time) const override; FileFlags fileFlags(FileFlags type = FileInfoAll) const override; QString fileName(FileName file = DefaultName) const override; QAbstractFileEngine::Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override; QAbstractFileEngine::Iterator *endEntryList() override; + private: - QString m_file; + void closeNativeFileDescriptor(); + QString m_initialFile; + QJNIObjectPrivate m_pfd; + DocumentFilePtr m_documentFile; }; class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler @@ -75,8 +92,48 @@ public: QString currentFileName() const override; QString currentFilePath() const override; private: - mutable QStringList m_entries; - mutable int m_index = -1; + mutable QList m_files; + mutable qsizetype m_index = -1; +}; + +/*! + * + * DocumentFile Api. + * Check https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile + * for more information. + * + */ +class DocumentFile : public std::enable_shared_from_this +{ +public: + static DocumentFilePtr parseFromAnyUri(const QString &filename); + static DocumentFilePtr fromSingleUri(const QJNIObjectPrivate &uri); + static DocumentFilePtr fromTreeUri(const QJNIObjectPrivate &treeUri); + + DocumentFilePtr createFile(const QString &mimeType, const QString &displayName); + DocumentFilePtr createDirectory(const QString &displayName); + const QJNIObjectPrivate &uri() const; + const DocumentFilePtr &parent() const; + QString name() const; + QString id() const; + QString mimeType() const; + bool isDirectory() const; + bool isFile() const; + bool isVirtual() const; + QDateTime lastModified() const; + int64_t length() const; + bool canRead() const; + bool canWrite() const; + bool remove(); + bool exists() const; + std::vector listFiles(); + +protected: + DocumentFile(const QJNIObjectPrivate &uri, const std::shared_ptr &parent); + +protected: + QJNIObjectPrivate m_uri; + DocumentFilePtr m_parent; }; #endif // ANDROIDCONTENTFILEENGINE_H -- cgit v1.2.3 From d50d4e8b9febb670e5a48fdf5b60dff8456d8692 Mon Sep 17 00:00:00 2001 From: Assam Boudjelthia Date: Tue, 22 Nov 2022 14:06:33 +0200 Subject: Android: handle rename() operation with content uris Allow renaming content uris if the destination is provided as a direct fileName (i.e. not full content scheme path), and if the destination has the same trailing path (or parent) which means a rename in the same folder structure. Task-number: QTBUG-98974 Change-Id: Ibc4973366807dd5284c19912ab04ff90f2a573cb Reviewed-by: Ville Voutilainen (cherry picked from commit c1fa5d602c541b06e3e2fc2d02f5d62060c84504) --- .../platforms/android/androidcontentfileengine.cpp | 57 ++++++++++++++++++++++ .../platforms/android/androidcontentfileengine.h | 2 + 2 files changed, 59 insertions(+) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index a1027229ee..ffda304308 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -167,6 +167,15 @@ bool AndroidContentFileEngine::remove() return m_documentFile->remove(); } +bool AndroidContentFileEngine::rename(const QString &newName) +{ + if (m_documentFile->rename(newName)) { + m_initialFile = m_documentFile->uri().toString(); + return true; + } + return false; +} + bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories) const { QString tmp = dirName; @@ -491,6 +500,7 @@ const QLatin1String COLUMN_SIZE("_size"); constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008; constexpr int FLAG_SUPPORTS_DELETE = 0x00000004; +constexpr int FLAG_SUPPORTS_RENAME = 0x00000040; constexpr int FLAG_SUPPORTS_WRITE = 0x00000002; constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200; @@ -582,6 +592,20 @@ bool deleteDocument(const QJNIObjectPrivate &documentUri) documentUri.object()); } +QJNIObjectPrivate renameDocument(const QJNIObjectPrivate &documentUri, const QString &displayName) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_RENAME)) + return {}; + + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "renameDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", + contentResolverInstance().object(), + documentUri.object(), + QJNIObjectPrivate::fromString(displayName).object()); +} } // End DocumentsContract namespace // Start of DocumentFile @@ -804,4 +828,37 @@ std::vector DocumentFile::listFiles() return res; } +bool DocumentFile::rename(const QString &newName) +{ + QJNIObjectPrivate uri; + if (newName.startsWith(QLatin1String("content://"))) { + auto lastSeparatorIndex = [](const QString &file) { + int posDecoded = file.lastIndexOf(QLatin1Char('/')); + int posEncoded = file.lastIndexOf(QUrl::toPercentEncoding(QLatin1String("/"))); + return posEncoded > posDecoded ? posEncoded : posDecoded; + }; + + // first try to see if the new file is under the same tree and thus used rename only + const QString parent = m_uri.toString().left(lastSeparatorIndex(m_uri.toString())); + if (newName.contains(parent)) { + QString displayName = newName.mid(lastSeparatorIndex(newName)); + if (displayName.startsWith(QLatin1Char('/'))) + displayName.remove(0, 1); + else if (displayName.startsWith(QUrl::toPercentEncoding(QLatin1String("/")))) + displayName.remove(0, 3); + + uri = renameDocument(m_uri, displayName); + } + } else { + uri = renameDocument(m_uri, newName); + } + + if (uri.isValid()) { + m_uri = uri; + return true; + } + + return false; +} + // End of DocumentFile diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index e6457bc56d..5b043e2fcb 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -56,6 +56,7 @@ public: bool close() override; qint64 size() const override; bool remove() override; + bool rename(const QString &newName) override; bool mkdir(const QString &dirName, bool createParentDirectories) const override; bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; QByteArray id() const override; @@ -127,6 +128,7 @@ public: bool remove(); bool exists() const; std::vector listFiles(); + bool rename(const QString &newName); protected: DocumentFile(const QJNIObjectPrivate &uri, const std::shared_ptr &parent); -- cgit v1.2.3 From f48abc46321502df2425aebc0999be2f5f3bf1f2 Mon Sep 17 00:00:00 2001 From: Assam Boudjelthia Date: Sat, 17 Dec 2022 02:59:54 +0200 Subject: Android: handle move operation with content uris Allow moving content uris if the destination is provided a full content uri with a parent that's different from the source content uri (i.e. different folders). Note: since the underlaying Android APIs don't always know about the parent of a uri, we do some step to deduce that, but that's not always guaranteed to work. Task-number: QTBUG-98974 Change-Id: If21954e5963f4eb0b96c7ccd983943ea2cab5b24 Reviewed-by: Ville Voutilainen (cherry picked from commit c203ec2720b694fd877512da531a227e0f3310cb) --- .../platforms/android/androidcontentfileengine.cpp | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index ffda304308..1e6cd9baba 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -500,6 +500,7 @@ const QLatin1String COLUMN_SIZE("_size"); constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008; constexpr int FLAG_SUPPORTS_DELETE = 0x00000004; +constexpr int FLAG_SUPPORTS_MOVE = 0x00000100; constexpr int FLAG_SUPPORTS_RENAME = 0x00000040; constexpr int FLAG_SUPPORTS_WRITE = 0x00000002; constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200; @@ -592,6 +593,24 @@ bool deleteDocument(const QJNIObjectPrivate &documentUri) documentUri.object()); } +QJNIObjectPrivate moveDocument(const QJNIObjectPrivate &sourceDocumentUri, + const QJNIObjectPrivate &sourceParentDocumentUri, + const QJNIObjectPrivate &targetParentDocumentUri) +{ + const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_MOVE)) + return {}; + + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "moveDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;Landroid/net/Uri;Landroid/net/Uri;)Landroid/net/Uri;", + contentResolverInstance().object(), + sourceDocumentUri.object(), + sourceParentDocumentUri.object(), + targetParentDocumentUri.object()); +} + QJNIObjectPrivate renameDocument(const QJNIObjectPrivate &documentUri, const QString &displayName) { const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); @@ -848,6 +867,12 @@ bool DocumentFile::rename(const QString &newName) displayName.remove(0, 3); uri = renameDocument(m_uri, displayName); + } else { + // Move + QJNIObjectPrivate srcParentUri = fromTreeUri(parseUri(parent))->uri(); + const QString destParent = newName.left(lastSeparatorIndex(newName)); + QJNIObjectPrivate targetParentUri = fromTreeUri(parseUri(destParent))->uri(); + uri = moveDocument(m_uri, srcParentUri, targetParentUri); } } else { uri = renameDocument(m_uri, newName); -- cgit v1.2.3 From b43405c8e90e5baba00308bdd6935a5efc300c68 Mon Sep 17 00:00:00 2001 From: Assam Boudjelthia Date: Thu, 8 Dec 2022 03:42:13 +0200 Subject: Android: pass EXTRA_INITIAL_URI to native FileDialog Allow setting the initial directory where the file dialog will be opened. Change-Id: I1395b367c74d28fb2890ac53a90456c3ac4c1b05 Reviewed-by: Andy Shaw (cherry picked from commit 609e14724edfd8d8cef23c5f30ad7812a359ed8d) Reviewed-by: Qt CI Bot Reviewed-by: Ville Voutilainen --- .../android/qandroidplatformfiledialoghelper.cpp | 28 ++++++++++++++++++---- .../android/qandroidplatformfiledialoghelper.h | 6 +++-- 2 files changed, 28 insertions(+), 6 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp index 17c4c7108a..4cf5591983 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp @@ -128,6 +128,22 @@ void QAndroidPlatformFileDialogHelper::setInitialFileName(const QString &title) extraTitle.object(), QJNIObjectPrivate::fromString(title).object()); } +void QAndroidPlatformFileDialogHelper::setInitialDirectoryUri(const QString &directory) +{ + if (directory.isEmpty()) + return; + + if (QtAndroidPrivate::androidSdkVersion() < 26) + return; + + const auto extraInitialUri = QJNIObjectPrivate::getStaticObjectField( + "android/provider/DocumentsContract", "EXTRA_INITIAL_URI", "Ljava/lang/String;"); + m_intent.callObjectMethod("putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + extraInitialUri.object(), + QJNIObjectPrivate::fromString(directory).object()); +} + void QAndroidPlatformFileDialogHelper::setOpenableCategory() { const QJNIObjectPrivate CATEGORY_OPENABLE = QJNIObjectPrivate::getStaticObjectField( @@ -210,11 +226,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win if (options()->acceptMode() == QFileDialogOptions::AcceptSave) { m_intent = getFileDialogIntent("ACTION_CREATE_DOCUMENT"); const QList selectedFiles = options()->initiallySelectedFiles(); - if (selectedFiles.size() > 0) { - // TODO: The initial folder to show at the start should be handled by EXTRA_INITIAL_URI - // Take only the file name. + if (selectedFiles.size() > 0) setInitialFileName(selectedFiles.first().fileName()); - } } else if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) { switch (options()->fileMode()) { case QFileDialogOptions::FileMode::DirectoryOnly: @@ -238,6 +251,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win setMimeTypes(); } + setInitialDirectoryUri(m_directory.toString()); + QtAndroidPrivate::registerActivityResultListener(this); m_activity.callMethod("startActivityForResult", "(Landroid/content/Intent;I)V", m_intent.object(), REQUEST_CODE); @@ -251,6 +266,11 @@ void QAndroidPlatformFileDialogHelper::hide() QtAndroidPrivate::unregisterActivityResultListener(this); } +void QAndroidPlatformFileDialogHelper::setDirectory(const QUrl &directory) +{ + m_directory = directory; +} + void QAndroidPlatformFileDialogHelper::exec() { m_eventLoop.exec(QEventLoop::DialogExec); diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h index 5ad1210210..a2dcf62108 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h @@ -67,8 +67,8 @@ public: void setFilter() override {}; QList selectedFiles() const override { return m_selectedFile; }; void selectFile(const QUrl &file) override { Q_UNUSED(file) }; - QUrl directory() const override { return QUrl(); }; - void setDirectory(const QUrl &directory) override { Q_UNUSED(directory) }; + QUrl directory() const override { return m_directory; } + void setDirectory(const QUrl &directory) override; bool defaultNameFilterDisables() const override { return false; }; bool handleActivityResult(jint requestCode, jint resultCode, jobject data) override; @@ -76,12 +76,14 @@ private: QJNIObjectPrivate getFileDialogIntent(const QString &intentType); void takePersistableUriPermission(const QJNIObjectPrivate &uri); void setInitialFileName(const QString &title); + void setInitialDirectoryUri(const QString &directory); void setOpenableCategory(); void setAllowMultipleSelections(bool allowMultiple); void setMimeTypes(); QEventLoop m_eventLoop; QList m_selectedFile; + QUrl m_directory; QJNIObjectPrivate m_intent; const QJNIObjectPrivate m_activity; }; -- cgit v1.2.3 From cc641080d4f3803f2d9683c2a9181fee7c4bd12a Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Tue, 24 Jan 2023 14:29:27 +0100 Subject: ODBC SQL driver: deal with different sizes of SQLTCHAR correctly Neither the UTF-32, nor the UTF-8 recoding of a UTF-16 string is necessarily of the same length as the input. The UTF-32 version may be shorter, if surrogate pairs were encountered. The UTF-8 version will be longer whenever the string contains non-US-ASCII characters. Split toSQLTCHAR() into three functions, templated on sizeof(SQLTCHAR), and use QVLA's range-append instead of manual memcpy()s. This patch specifically doesn't use constexpr-if, as that's not available until C++17, which Qt 5 doesn't require. Change-Id: I0bfcb66eb321598908ef00ac34c888fdbccf9316 Reviewed-by: Christian Ehrlicher Reviewed-by: Thiago Macieira (cherry picked from commit 66767eea46bea0f19f8ae5ad6ebc641d86867701) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/sqldrivers/odbc/qsql_odbc.cpp | 44 +++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index 5eed2f9689..a15baea671 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -92,23 +92,39 @@ inline static QString fromSQLTCHAR(const QVarLengthArray& input, int s return result; } +template +void toSQLTCHARImpl(QVarLengthArray &result, const QString &input); // primary template undefined + +template +void do_append(QVarLengthArray &result, const Container &c) +{ + result.append(reinterpret_cast(c.data()), c.size()); +} + +template <> +void toSQLTCHARImpl<1>(QVarLengthArray &result, const QString &input) +{ + const auto u8 = input.toUtf8(); + do_append(result, u8); +} + +template <> +void toSQLTCHARImpl<2>(QVarLengthArray &result, const QString &input) +{ + do_append(result, input); +} + +template <> +void toSQLTCHARImpl<4>(QVarLengthArray &result, const QString &input) +{ + const auto u32 = input.toUcs4(); + do_append(result, u32); +} + inline static QVarLengthArray toSQLTCHAR(const QString &input) { QVarLengthArray result; - result.resize(input.size()); - switch(sizeof(SQLTCHAR)) { - case 1: - memcpy(result.data(), input.toUtf8().data(), input.size()); - break; - case 2: - memcpy(result.data(), input.unicode(), input.size() * 2); - break; - case 4: - memcpy(result.data(), input.toUcs4().data(), input.size() * 4); - break; - default: - qCritical("sizeof(SQLTCHAR) is %d. Don't know how to handle this.", int(sizeof(SQLTCHAR))); - } + toSQLTCHARImpl(result, input); result.append(0); // make sure it's null terminated, doesn't matter if it already is, it does if it isn't. return result; } -- cgit v1.2.3 From 99cc3802d127a8569dcba267c0338e9333d36d51 Mon Sep 17 00:00:00 2001 From: Phan Quang Minh Date: Fri, 7 Oct 2022 13:52:38 +0700 Subject: ibus: check object validity before honoring `inputMethodAccepted()` `inputMethodAccepted()` always return false if there is no current focus object. This means QIBusPlatformInputContext will fail to send the necessary `FocusOut` request to the input context, causing IBus to not send the appropriate content type information to the panel service when the application regains focus and issue a `FocusIn` request. This results in issues like https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5346, which can happen when a Qt application is in foreground as the IBus daemon fails to propagate the content type information to GNOME Shell, which acts as the panel service on GNOME systems. Fix this by checking for the validity of `object` before honoring the result of `inputMethodAccepted()`. Change-Id: I6b79ffc7c5f03ffc05527c29e52a0859f3594bfa Reviewed-by: Liang Qi (cherry picked from commit 95b4cfb1af9aefe3ff3aa151804f464388329c63) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp index f39737785c..e02b669e61 100644 --- a/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp +++ b/src/plugins/platforminputcontexts/ibus/qibusplatforminputcontext.cpp @@ -289,7 +289,7 @@ void QIBusPlatformInputContext::setFocusObject(QObject *object) // It would seem natural here to call FocusOut() on the input method if we // transition from an IME accepted focus object to one that does not accept it. // Mysteriously however that is not sufficient to fix bug QTBUG-63066. - if (!inputMethodAccepted()) + if (object && !inputMethodAccepted()) return; if (debug) -- cgit v1.2.3 From ae739feca4b2685999181b0e010d82d8caad5793 Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Thu, 15 Dec 2022 16:18:49 +0800 Subject: GTK3 theme: simplify code There's no need to first convert to QString and then convert back to QByteArray. Change-Id: Idedcf3143f44c640a9259f16e364dfe76ecf4c0d Reviewed-by: Liang Qi (cherry picked from commit e86a5f5f8b184562b5cde0da8882a2d8ebce84d5) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platformthemes/gtk3/qgtk3theme.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp index 5a1f707eef..299097a9be 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp @@ -115,7 +115,7 @@ QGtk3Theme::QGtk3Theme() if (qEnvironmentVariableIsEmpty("XCURSOR_SIZE")) { const int cursorSize = gtkSetting("gtk-cursor-theme-size"); if (cursorSize > 0) - qputenv("XCURSOR_SIZE", QString::number(cursorSize).toUtf8()); + qputenv("XCURSOR_SIZE", QByteArray::number(cursorSize)); } if (qEnvironmentVariableIsEmpty("XCURSOR_THEME")) { const QString cursorTheme = gtkSetting("gtk-cursor-theme-name"); -- cgit v1.2.3 From d91f5e08da0e56db450129041acb4a5cdc2abae9 Mon Sep 17 00:00:00 2001 From: Christian Ehrlicher Date: Tue, 27 Sep 2022 19:06:17 +0200 Subject: QODBC: correctly fill cache when index() is called QODBCResult::isNull() incorrectly checked for the validity of the internal row cache which lead to wrong results when the requested column was not yet cached. Change-Id: Ic7dcc2117e6f05b63c83f21c6a84ba7e0bda2b2d Reviewed-by: Volker Hilsheimer (cherry picked from commit 91374bb6322cf09525e99698c8fae7688c227d74) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/sqldrivers/odbc/qsql_odbc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/plugins') diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index a15baea671..2cddefd6e7 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -1318,7 +1318,7 @@ bool QODBCResult::isNull(int field) Q_D(const QODBCResult); if (field < 0 || field >= d->fieldCache.size()) return true; - if (field <= d->fieldCacheIdx) { + if (field >= d->fieldCacheIdx) { // since there is no good way to find out whether the value is NULL // without fetching the field we'll fetch it here. // (data() also sets the NULL flag) -- cgit v1.2.3 From 54fd964b179569dec2dc3e8e301e6ff928278323 Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Tue, 31 Jan 2023 11:06:56 +0100 Subject: ODBC SQL driver: fix conversion of QByteArray to QVLA The QByteArray is assumed to contain an SQLTCHAR string (so, either UTF-8, UTF-16 or UTF-32-encoded). Only in the UTF-8 case would the size of the byte array be the same as the size of the SQLTCHAR string in codepoints, yet the size in bytes is what the code passed to the QVLA append() call, causing it to read past the QByteArray buffer in the UTF-16 and UTF-32 cases. Fix by properly calculating the string size from the size-in-bytes and then memcpy()ing into the QVLA. We use memcpy() and not QVLA::append(T*, n) because the QByteArray buffer need not be aligned on an alignof(SQLTCHAR) boundary (certainly not since it gained the prepend "optimization"). Manual conflict resolutions: - dealt with 32-bit-ness of container size_type vs. Qt 6's 64-bit Change-Id: If3838c3dee89e6aca65541242642315b8e1fa6b4 Reviewed-by: Thiago Macieira (cherry picked from commit 4c445ef0bae8b36ec4a742552f0ebd81a1a90723) Reviewed-by: Volker Hilsheimer --- src/plugins/sqldrivers/odbc/qsql_odbc.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index 2cddefd6e7..fdef313d46 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -1732,10 +1732,11 @@ bool QODBCResult::exec() case QVariant::String: if (d->unicode) { if (bindValueType(i) & QSql::Out) { - const QByteArray &first = tmpStorage.at(i); - QVarLengthArray array; - array.append((const SQLTCHAR *)first.constData(), first.size()); - values[i] = fromSQLTCHAR(array, first.size()/sizeof(SQLTCHAR)); + const QByteArray &bytes = tmpStorage.at(i); + const auto strSize = bytes.size() / int(sizeof(SQLTCHAR)); + QVarLengthArray string(strSize); + memcpy(string.data(), bytes.data(), strSize * sizeof(SQLTCHAR)); + values[i] = fromSQLTCHAR(string); } break; } -- cgit v1.2.3 From 458e2714e1a9c10647bc112a1d33afc7191be4c6 Mon Sep 17 00:00:00 2001 From: Marc Mutz Date: Mon, 30 Jan 2023 15:37:13 +0100 Subject: SQL/ODBC: fix some users of toSQLTCHAR() to not assume identical UTF-8/16/32 string lengths We already fixed the implementation of toSQLTCHAR() in 66767eea46bea0f19f8ae5ad6ebc641d86867701 to not assume that a UTF-8 or UTF-32-encoded string has the same number of code points as the equivalent UTF-16 string, but it turns out that users of the function, as well as other code, also failed to account for this. This patch fixes callers of toSQLTCHAR() to use const auto encoded = toSQLTCHAR(s); ~~~ use encoded.data(), encoded.size() ~~~ (except we can't make `encoded` const, because the SQL API isn't const-correct and takes void* instead of const void*) instead of the anti-pattern ~~~ use toSQLTCHAR(s).data(), s.size() ~~~ As a drive-by: - Extract Method qt_string_SQLSetConnectAttr() - skipping an unneeded .utf16() call (a NUL-terminated string is not required for calling toSQLTCHAR()) - de-duplicate some code in exec() - and make a comment there slightly more informative - replace - NULL with nullptr - size() == 0 with isEmpty() - C-style with constructor-style casts Manual conflict resolutions: - _L1 vs. QLatin1String - u'x' vs. QLatin1Char('x') - qsizetype vs. int Change-Id: I3696381d0a93af8861ce2b7915f212d9e5e9a243 Reviewed-by: Thiago Macieira (cherry picked from commit 46af1fe49f7f419dc1b3231de9860e2da0ea48f8) Reviewed-by: Andy Shaw --- src/plugins/sqldrivers/odbc/qsql_odbc.cpp | 153 ++++++++++++++++-------------- 1 file changed, 83 insertions(+), 70 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index fdef313d46..58b220ad3e 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -779,6 +779,14 @@ QChar QODBCDriverPrivate::quoteChar() return quote; } +static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, const QString &val) +{ + auto encoded = toSQLTCHAR(val); + return SQLSetConnectAttr(handle, attr, + encoded.data(), + SQLINTEGER(encoded.size() * sizeof(SQLTCHAR))); // size in bytes +} + bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) { @@ -814,10 +822,7 @@ bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_LOGIN_TIMEOUT, (SQLPOINTER) size_t(v), 0); } else if (opt.toUpper() == QLatin1String("SQL_ATTR_CURRENT_CATALOG")) { - val.utf16(); // 0 terminate - r = SQLSetConnectAttr(hDbc, SQL_ATTR_CURRENT_CATALOG, - toSQLTCHAR(val).data(), - val.length()*sizeof(SQLTCHAR)); + r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_CURRENT_CATALOG, val); } else if (opt.toUpper() == QLatin1String("SQL_ATTR_METADATA_ID")) { if (val.toUpper() == QLatin1String("SQL_TRUE")) { v = SQL_TRUE; @@ -832,10 +837,7 @@ bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_PACKET_SIZE, (SQLPOINTER) size_t(v), 0); } else if (opt.toUpper() == QLatin1String("SQL_ATTR_TRACEFILE")) { - val.utf16(); // 0 terminate - r = SQLSetConnectAttr(hDbc, SQL_ATTR_TRACEFILE, - toSQLTCHAR(val).data(), - val.length()*sizeof(SQLTCHAR)); + r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_TRACEFILE, val); } else if (opt.toUpper() == QLatin1String("SQL_ATTR_TRACE")) { if (val.toUpper() == QLatin1String("SQL_OPT_TRACE_OFF")) { v = SQL_OPT_TRACE_OFF; @@ -1038,9 +1040,12 @@ bool QODBCResult::reset (const QString& query) return false; } - r = SQLExecDirect(d->hStmt, - toSQLTCHAR(query).data(), - (SQLINTEGER) query.length()); + { + auto encoded = toSQLTCHAR(query); + r = SQLExecDirect(d->hStmt, + encoded.data(), + SQLINTEGER(encoded.size())); + } if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r!= SQL_NO_DATA) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to execute statement"), QSqlError::StatementError, d)); @@ -1387,9 +1392,12 @@ bool QODBCResult::prepare(const QString& query) return false; } - r = SQLPrepare(d->hStmt, - toSQLTCHAR(query).data(), - (SQLINTEGER) query.length()); + { + auto encoded = toSQLTCHAR(query); + r = SQLPrepare(d->hStmt, + encoded.data(), + SQLINTEGER(encoded.size())); + } if (r != SQL_SUCCESS) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", @@ -1417,7 +1425,7 @@ bool QODBCResult::exec() SQLCloseCursor(d->hStmt); QVector& values = boundValues(); - QVector tmpStorage(values.count(), QByteArray()); // holds temporary buffers + QVector tmpStorage(values.count(), QByteArray()); // targets for SQLBindParameter() QVarLengthArray indicators(values.count()); memset(indicators.data(), 0, indicators.size() * sizeof(SQLLEN)); @@ -1596,35 +1604,36 @@ bool QODBCResult::exec() case QVariant::String: if (d->unicode) { QByteArray &ba = tmpStorage[i]; - QString str = val.toString(); + { + const auto encoded = toSQLTCHAR(val.toString()); + ba = QByteArray(reinterpret_cast(encoded.data()), + encoded.size() * sizeof(SQLTCHAR)); + } + if (*ind != SQL_NULL_DATA) - *ind = str.length() * sizeof(SQLTCHAR); - int strSize = str.length() * sizeof(SQLTCHAR); + *ind = ba.size(); if (bindValueType(i) & QSql::Out) { - const QVarLengthArray a(toSQLTCHAR(str)); - ba = QByteArray((const char *)a.constData(), a.size() * sizeof(SQLTCHAR)); r = SQLBindParameter(d->hStmt, i + 1, qParamType[bindValueType(i) & QSql::InOut], SQL_C_TCHAR, - strSize > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, + ba.size() > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, 0, // god knows... don't change this! 0, - ba.data(), + const_cast(ba.constData()), // don't detach ba.size(), ind); break; } - ba = QByteArray ((const char *)toSQLTCHAR(str).constData(), str.size()*sizeof(SQLTCHAR)); r = SQLBindParameter(d->hStmt, i + 1, qParamType[bindValueType(i) & QSql::InOut], SQL_C_TCHAR, - strSize > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, - strSize, + ba.size() > 254 ? SQL_WLONGVARCHAR : SQL_WVARCHAR, + ba.size(), 0, - const_cast(ba.constData()), + const_cast(ba.constData()), // don't detach ba.size(), ind); break; @@ -1983,14 +1992,16 @@ bool QODBCDriver::open(const QString & db, SQLSMALLINT cb; QVarLengthArray connOut(1024); memset(connOut.data(), 0, connOut.size() * sizeof(SQLTCHAR)); - r = SQLDriverConnect(d->hDbc, - NULL, - toSQLTCHAR(connQStr).data(), - (SQLSMALLINT)connQStr.length(), - connOut.data(), - 1024, - &cb, - /*SQL_DRIVER_NOPROMPT*/0); + { + auto encoded = toSQLTCHAR(connQStr); + r = SQLDriverConnect(d->hDbc, + nullptr, + encoded.data(), SQLSMALLINT(encoded.size()), + connOut.data(), + 1024, + &cb, + /*SQL_DRIVER_NOPROMPT*/0); + } if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { setLastError(qMakeError(tr("Unable to connect"), QSqlError::ConnectionError, d)); @@ -2369,17 +2380,15 @@ QStringList QODBCDriver::tables(QSql::TableType type) const if (tableType.isEmpty()) return tl; - QString joinedTableTypeString = tableType.join(QLatin1Char(',')); + { + auto joinedTableTypeString = toSQLTCHAR(tableType.join(u',')); - r = SQLTables(hStmt, - NULL, - 0, - NULL, - 0, - NULL, - 0, - toSQLTCHAR(joinedTableTypeString).data(), - joinedTableTypeString.length() /* characters, not bytes */); + r = SQLTables(hStmt, + nullptr, 0, + nullptr, 0, + nullptr, 0, + joinedTableTypeString.data(), joinedTableTypeString.size()); + } if (r != SQL_SUCCESS) qSqlWarning(QLatin1String("QODBCDriver::tables Unable to execute table list"), d); @@ -2453,28 +2462,30 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const SQL_ATTR_CURSOR_TYPE, (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, SQL_IS_UINTEGER); - r = SQLPrimaryKeys(hStmt, - catalog.length() == 0 ? NULL : toSQLTCHAR(catalog).data(), - catalog.length(), - schema.length() == 0 ? NULL : toSQLTCHAR(schema).data(), - schema.length(), - toSQLTCHAR(table).data(), - table.length() /* in characters, not in bytes */); + { + auto c = toSQLTCHAR(catalog); + auto s = toSQLTCHAR(schema); + auto t = toSQLTCHAR(table); + r = SQLPrimaryKeys(hStmt, + catalog.isEmpty() ? nullptr : c.data(), c.size(), + schema.isEmpty() ? nullptr : s.data(), s.size(), + t.data(), t.size()); + } // if the SQLPrimaryKeys() call does not succeed (e.g the driver // does not support it) - try an alternative method to get hold of // the primary index (e.g MS Access and FoxPro) if (r != SQL_SUCCESS) { - r = SQLSpecialColumns(hStmt, - SQL_BEST_ROWID, - catalog.length() == 0 ? NULL : toSQLTCHAR(catalog).data(), - catalog.length(), - schema.length() == 0 ? NULL : toSQLTCHAR(schema).data(), - schema.length(), - toSQLTCHAR(table).data(), - table.length(), - SQL_SCOPE_CURROW, - SQL_NULLABLE); + auto c = toSQLTCHAR(catalog); + auto s = toSQLTCHAR(schema); + auto t = toSQLTCHAR(table); + r = SQLSpecialColumns(hStmt, + SQL_BEST_ROWID, + catalog.isEmpty() ? nullptr : c.data(), c.size(), + schema.isEmpty() ? nullptr : s.data(), s.size(), + t.data(), t.size(), + SQL_SCOPE_CURROW, + SQL_NULLABLE); if (r != SQL_SUCCESS) { qSqlWarning(QLatin1String("QODBCDriver::primaryIndex: Unable to execute primary key list"), d); @@ -2555,15 +2566,17 @@ QSqlRecord QODBCDriver::record(const QString& tablename) const SQL_ATTR_CURSOR_TYPE, (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, SQL_IS_UINTEGER); - r = SQLColumns(hStmt, - catalog.length() == 0 ? NULL : toSQLTCHAR(catalog).data(), - catalog.length(), - schema.length() == 0 ? NULL : toSQLTCHAR(schema).data(), - schema.length(), - toSQLTCHAR(table).data(), - table.length(), - NULL, - 0); + { + auto c = toSQLTCHAR(catalog); + auto s = toSQLTCHAR(schema); + auto t = toSQLTCHAR(table); + r = SQLColumns(hStmt, + catalog.isEmpty() ? nullptr : c.data(), c.size(), + schema.isEmpty() ? nullptr : s.data(), s.size(), + t.data(), t.size(), + nullptr, + 0); + } if (r != SQL_SUCCESS) qSqlWarning(QLatin1String("QODBCDriver::record: Unable to execute column list"), d); -- cgit v1.2.3 From c724f6ec20f104ff9555e1f0e95cce54494f32a8 Mon Sep 17 00:00:00 2001 From: Roland Pallai Date: Fri, 27 Jan 2023 14:29:56 +0100 Subject: QXcbConnection::getTimestamp: do not return stale timestamp The problem is `PropertyNotify` event generated by `xcb_delete_property()` at return could be reported as an actual timestamp at next call due to a missing `state` filter. Because `PropertyNotify` is generated even if an unchanged property value is set we can get rid of this delete event easily. This issue causes observable problems in kwin_x11 (KDE project) too. Fixes: QTBUG-56595 Change-Id: Ice6cfa934e3769d86e9f7264e369dc5918c8542a Reviewed-by: Giuseppe D'Angelo Reviewed-by: Liang Qi Reviewed-by: JiDe Zhang (cherry picked from commit 03ac8c7397b9f070a8ef5f33b4675411293b0723) Reviewed-by: Qt Cherry-pick Bot --- src/plugins/platforms/xcb/qxcbatom.cpp | 2 ++ src/plugins/platforms/xcb/qxcbatom.h | 2 ++ src/plugins/platforms/xcb/qxcbconnection.cpp | 6 ++---- 3 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/xcb/qxcbatom.cpp b/src/plugins/platforms/xcb/qxcbatom.cpp index 3a60080ec0..7e05a9f010 100644 --- a/src/plugins/platforms/xcb/qxcbatom.cpp +++ b/src/plugins/platforms/xcb/qxcbatom.cpp @@ -90,6 +90,8 @@ static const char *xcb_atomnames = { "_QT_CLOSE_CONNECTION\0" + "_QT_GET_TIMESTAMP\0" + "_MOTIF_WM_HINTS\0" "DTWM_IS_RUNNING\0" diff --git a/src/plugins/platforms/xcb/qxcbatom.h b/src/plugins/platforms/xcb/qxcbatom.h index 5bda9e0a69..6c2a2367e2 100644 --- a/src/plugins/platforms/xcb/qxcbatom.h +++ b/src/plugins/platforms/xcb/qxcbatom.h @@ -91,6 +91,8 @@ public: // Qt/XCB specific _QT_CLOSE_CONNECTION, + _QT_GET_TIMESTAMP, + _MOTIF_WM_HINTS, DTWM_IS_RUNNING, diff --git a/src/plugins/platforms/xcb/qxcbconnection.cpp b/src/plugins/platforms/xcb/qxcbconnection.cpp index f51ffc52c1..235d6c1570 100644 --- a/src/plugins/platforms/xcb/qxcbconnection.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection.cpp @@ -802,8 +802,8 @@ xcb_timestamp_t QXcbConnection::getTimestamp() { // send a dummy event to myself to get the timestamp from X server. xcb_window_t window = rootWindow(); - xcb_atom_t dummyAtom = atom(QXcbAtom::CLIP_TEMPORARY); - xcb_change_property(xcb_connection(), XCB_PROP_MODE_APPEND, window, dummyAtom, + xcb_atom_t dummyAtom = atom(QXcbAtom::_QT_GET_TIMESTAMP); + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, dummyAtom, XCB_ATOM_INTEGER, 32, 0, nullptr); connection()->flush(); @@ -835,8 +835,6 @@ xcb_timestamp_t QXcbConnection::getTimestamp() xcb_timestamp_t timestamp = pn->time; free(event); - xcb_delete_property(xcb_connection(), window, dummyAtom); - return timestamp; } -- cgit v1.2.3