summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/android
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/platforms/android')
-rw-r--r--src/plugins/platforms/android/androidcontentfileengine.cpp776
-rw-r--r--src/plugins/platforms/android/androidcontentfileengine.h65
-rw-r--r--src/plugins/platforms/android/androidjniaccessibility.cpp42
-rw-r--r--src/plugins/platforms/android/androidjniaccessibility.h1
-rw-r--r--src/plugins/platforms/android/androidjniclipboard.cpp22
-rw-r--r--src/plugins/platforms/android/androidjnimain.cpp41
-rw-r--r--src/plugins/platforms/android/androidjnimain.h1
-rw-r--r--src/plugins/platforms/android/androidjnimenu.cpp4
-rw-r--r--src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp11
-rw-r--r--src/plugins/platforms/android/qandroideventdispatcher.cpp6
-rw-r--r--src/plugins/platforms/android/qandroidplatformaccessibility.cpp2
-rw-r--r--src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp28
-rw-r--r--src/plugins/platforms/android/qandroidplatformfiledialoghelper.h6
-rw-r--r--src/plugins/platforms/android/qandroidplatformintegration.cpp14
-rw-r--r--src/plugins/platforms/android/qandroidplatformintegration.h5
-rw-r--r--src/plugins/platforms/android/qandroidplatformscreen.cpp8
-rw-r--r--src/plugins/platforms/android/qandroidplatformservices.cpp11
-rw-r--r--src/plugins/platforms/android/qandroidplatformtheme.cpp85
-rw-r--r--src/plugins/platforms/android/qandroidplatformtheme.h9
19 files changed, 995 insertions, 142 deletions
diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp
index 04862ccba6..2be7c45bb2 100644
--- a/src/plugins/platforms/android/androidcontentfileengine.cpp
+++ b/src/plugins/platforms/android/androidcontentfileengine.cpp
@@ -1,5 +1,5 @@
-// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
-// Copyright (C) 2021 The Qt Company Ltd.
+// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
+// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "androidcontentfileengine.h"
@@ -7,16 +7,36 @@
#include <QtCore/qcoreapplication.h>
#include <QtCore/qjnienvironment.h>
#include <QtCore/qjniobject.h>
-
-#include <QDebug>
+#include <QtCore/qurl.h>
+#include <QtCore/qdatetime.h>
+#include <QtCore/qmimedatabase.h>
using namespace QNativeInterface;
using namespace Qt::StringLiterals;
-AndroidContentFileEngine::AndroidContentFileEngine(const QString &f)
- : m_file(f)
+Q_DECLARE_JNI_TYPE(ContentResolverType, "Landroid/content/ContentResolver;");
+Q_DECLARE_JNI_TYPE(UriType, "Landroid/net/Uri;");
+Q_DECLARE_JNI_CLASS(Uri, "android/net/Uri");
+Q_DECLARE_JNI_TYPE(ParcelFileDescriptorType, "Landroid/os/ParcelFileDescriptor;");
+Q_DECLARE_JNI_TYPE(CursorType, "Landroid/database/Cursor;");
+Q_DECLARE_JNI_TYPE(StringArray, "[Ljava/lang/String;");
+
+static QJniObject &contentResolverInstance()
+{
+ static QJniObject contentResolver;
+ if (!contentResolver.isValid()) {
+ contentResolver = QJniObject(QNativeInterface::QAndroidApplication::context())
+ .callMethod<QtJniTypes::ContentResolverType>("getContentResolver");
+ }
+
+ return contentResolver;
+}
+
+AndroidContentFileEngine::AndroidContentFileEngine(const QString &filename)
+ : m_initialFile(filename),
+ m_documentFile(DocumentFile::parseFromAnyUri(filename))
{
- setFileName(f);
+ setFileName(filename);
}
bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
@@ -29,6 +49,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
}
if (openMode & QFileDevice::WriteOnly) {
openModeStr += u'w';
+ if (!m_documentFile->exists()) {
+ if (QUrl(m_initialFile).path().startsWith("/tree/"_L1)) {
+ const int lastSeparatorIndex = m_initialFile.lastIndexOf('/');
+ 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 = "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 += u't';
@@ -36,21 +77,19 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
openModeStr += u'a';
}
- m_pfd = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative",
- "openParcelFdForContentUrl",
- "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;",
- QAndroidApplication::context(),
- QJniObject::fromString(fileName(DefaultName)).object(),
- QJniObject::fromString(openModeStr).object());
+ m_pfd = contentResolverInstance().callMethod<
+ QtJniTypes::ParcelFileDescriptorType, QtJniTypes::UriType, jstring>(
+ "openFileDescriptor",
+ m_documentFile->uri().object(),
+ QJniObject::fromString(openModeStr).object<jstring>());
if (!m_pfd.isValid())
return false;
- const auto fd = m_pfd.callMethod<jint>("getFd", "()I");
+ const auto fd = m_pfd.callMethod<jint>("getFd");
if (fd < 0) {
- m_pfd.callMethod<void>("close", "()V");
- m_pfd = QJniObject();
+ closeNativeFileDescriptor();
return false;
}
@@ -59,47 +98,130 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
bool AndroidContentFileEngine::close()
{
+ closeNativeFileDescriptor();
+ return QFSFileEngine::close();
+}
+
+void AndroidContentFileEngine::closeNativeFileDescriptor()
+{
if (m_pfd.isValid()) {
- m_pfd.callMethod<void>("close", "()V");
+ m_pfd.callMethod<void>("close");
m_pfd = QJniObject();
}
-
- return QFSFileEngine::close();
}
qint64 AndroidContentFileEngine::size() const
{
- const jlong size = QJniObject::callStaticMethod<jlong>(
- "org/qtproject/qt/android/QtNative", "getSize",
- "(Landroid/content/Context;Ljava/lang/String;)J", QAndroidApplication::context(),
- QJniObject::fromString(fileName(DefaultName)).object());
- return (qint64)size;
+ return m_documentFile->length();
+}
+
+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,
+ std::optional<QFileDevice::Permissions> permissions) const
+{
+ Q_UNUSED(permissions)
+
+ QString tmp = dirName;
+ tmp.remove(m_initialFile);
+
+ QStringList dirParts = tmp.split(u'/');
+ 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 = QJniObject::callStaticMethod<jboolean>(
- "org/qtproject/qt/android/QtNative", "checkIfDir",
- "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
- QJniObject::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 : QJniObject::callStaticMethod<jboolean>(
- "org/qtproject/qt/android/QtNative", "checkFileExists",
- "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
- QJniObject::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 = QJniObject::callStaticMethod<jboolean>(
- "org/qtproject/qt/android/QtNative", "checkIfWritable",
- "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
- QJniObject::fromString(fileName(DefaultName)).object());
- if (writable)
+ flags |= FileType;
+ if (m_documentFile->canWrite())
flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm;
}
return type & flags;
@@ -114,18 +236,18 @@ QString AndroidContentFileEngine::fileName(FileName f) const
case DefaultName:
case AbsoluteName:
case CanonicalName:
- return m_file;
+ return m_documentFile->uri().toString();
case BaseName:
- {
- const qsizetype pos = m_file.lastIndexOf(u'/');
- 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);
}
@@ -166,37 +288,539 @@ 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 = QJniObject::callStaticMethod<jboolean>(
- "org/qtproject/qt/android/QtNative", "checkIfDir",
- "(Landroid/content/Context;Ljava/lang/String;)Z",
- QAndroidApplication::context(),
- QJniObject::fromString(path()).object());
- if (isDir) {
- QJniObject objArray = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative",
- "listContentsFromTreeUri",
- "(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;",
- QAndroidApplication::context(),
- QJniObject::fromString(path()).object());
- if (objArray.isValid()) {
- QJniEnvironment env;
- const jsize length = env->GetArrayLength(objArray.object<jarray>());
- for (int i = 0; i != length; ++i) {
- m_entries << QJniObject(env->GetObjectArrayElement(
- objArray.object<jobjectArray>(), 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 QJniObject &object)
+ : m_object{object} { }
+
+ ~Cursor()
+ {
+ if (m_object.isValid())
+ m_object.callMethod<void>("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
+ {
+ int type = m_object.callMethod<jint>("getType", columnIndex);
+ switch (type) {
+ case FIELD_TYPE_NULL:
+ return {};
+ case FIELD_TYPE_INTEGER:
+ return QVariant::fromValue(m_object.callMethod<jlong>("getLong", columnIndex));
+ case FIELD_TYPE_FLOAT:
+ return QVariant::fromValue(m_object.callMethod<jdouble>("getDouble", columnIndex));
+ case FIELD_TYPE_STRING:
+ return QVariant::fromValue(m_object.callMethod<jstring>("getString",
+ columnIndex).toString());
+ case FIELD_TYPE_BLOB: {
+ auto blob = m_object.callMethod<jbyteArray>("getBlob", columnIndex);
+ QJniEnvironment env;
+ const auto blobArray = blob.object<jbyteArray>();
+ const int size = env->GetArrayLength(blobArray);
+ const auto byteArray = env->GetByteArrayElements(blobArray, nullptr);
+ QByteArray data{reinterpret_cast<const char *>(byteArray), size};
+ env->ReleaseByteArrayElements(blobArray, byteArray, 0);
+ return QVariant::fromValue(data);
+ }
+ }
+ return {};
+ }
+
+ static std::unique_ptr<Cursor> queryUri(const QJniObject &uri,
+ const QStringList &projection = {},
+ const QString &selection = {},
+ const QStringList &selectionArgs = {},
+ const QString &sortOrder = {})
+ {
+ auto cursor = contentResolverInstance().callMethod<QtJniTypes::CursorType>(
+ "query",
+ uri.object<QtJniTypes::UriType>(),
+ projection.isEmpty() ?
+ nullptr : fromStringList(projection).object<QtJniTypes::StringArray>(),
+ selection.isEmpty() ? nullptr : QJniObject::fromString(selection).object<jstring>(),
+ selectionArgs.isEmpty() ?
+ nullptr : fromStringList(selectionArgs).object<QtJniTypes::StringArray>(),
+ sortOrder.isEmpty() ? nullptr : QJniObject::fromString(sortOrder).object<jstring>());
+ if (!cursor.isValid())
+ return {};
+ return std::make_unique<Cursor>(cursor);
+ }
+
+ static QVariant queryColumn(const QJniObject &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<jboolean>("isNull", columnIndex);
+ }
+
+ int columnCount() const { return m_object.callMethod<jint>("getColumnCount"); }
+ int rowCount() const { return m_object.callMethod<jint>("getCount"); }
+ int row() const { return m_object.callMethod<jint>("getPosition"); }
+ bool isFirst() const { return m_object.callMethod<jboolean>("isFirst"); }
+ bool isLast() const { return m_object.callMethod<jboolean>("isLast"); }
+ bool moveToFirst() { return m_object.callMethod<jboolean>("moveToFirst"); }
+ bool moveToLast() { return m_object.callMethod<jboolean>("moveToLast"); }
+ bool moveToNext() { return m_object.callMethod<jboolean>("moveToNext"); }
+
+private:
+ static QJniObject fromStringList(const QStringList &list)
+ {
+ QJniEnvironment env;
+ auto array = env->NewObjectArray(list.size(), env.findClass("java/lang/String"), nullptr);
+ for (int i = 0; i < list.size(); ++i)
+ env->SetObjectArrayElement(array, i, QJniObject::fromString(list[i]).object());
+ return QJniObject::fromLocalRef(array);
+ }
+
+ QJniObject m_object;
+};
+
+// End of Cursor
+
+// Start of DocumentsContract
+
+Q_DECLARE_JNI_CLASS(DocumentsContract, "android/provider/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_MOVE = 0x00000100;
+constexpr int FLAG_SUPPORTS_RENAME = 0x00000040;
+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 QJniObject &uri)
+{
+ return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "getDocumentId",
+ uri.object()).toString();
+}
+
+QString treeDocumentId(const QJniObject &uri)
+{
+ return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "getTreeDocumentId",
+ uri.object()).toString();
+}
+
+QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId)
+{
+ return QJniObject::callStaticMethod<QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "buildChildDocumentsUriUsingTree",
+ uri.object<QtJniTypes::UriType>(),
+ QJniObject::fromString(parentDocumentId).object<jstring>());
+
+}
+
+QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId)
+{
+ return QJniObject::callStaticMethod<QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "buildDocumentUriUsingTree",
+ treeUri.object<QtJniTypes::UriType>(),
+ QJniObject::fromString(documentId).object<jstring>());
+}
+
+bool isDocumentUri(const QJniObject &uri)
+{
+ return QJniObject::callStaticMethod<jboolean>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "isDocumentUri",
+ QNativeInterface::QAndroidApplication::context(),
+ uri.object<QtJniTypes::UriType>());
+}
+
+bool isTreeUri(const QJniObject &uri)
+{
+ return QJniObject::callStaticMethod<jboolean>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "isTreeUri",
+ uri.object<QtJniTypes::UriType>());
}
+
+QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType,
+ const QString &displayName)
+{
+ return QJniObject::callStaticMethod<QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "createDocument",
+ contentResolverInstance().object<QtJniTypes::ContentResolverType>(),
+ parentDocumentUri.object<QtJniTypes::UriType>(),
+ QJniObject::fromString(mimeType).object<jstring>(),
+ QJniObject::fromString(displayName).object<jstring>());
+}
+
+bool deleteDocument(const QJniObject &documentUri)
+{
+ const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
+ if (!(flags & Document::FLAG_SUPPORTS_DELETE))
+ return {};
+
+ return QJniObject::callStaticMethod<jboolean>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "deleteDocument",
+ contentResolverInstance().object<QtJniTypes::ContentResolverType>(),
+ documentUri.object<QtJniTypes::UriType>());
+}
+
+QJniObject moveDocument(const QJniObject &sourceDocumentUri,
+ const QJniObject &sourceParentDocumentUri,
+ const QJniObject &targetParentDocumentUri)
+{
+ const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt();
+ if (!(flags & Document::FLAG_SUPPORTS_MOVE))
+ return {};
+
+ return QJniObject::callStaticMethod<QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "moveDocument",
+ contentResolverInstance().object<QtJniTypes::ContentResolverType>(),
+ sourceDocumentUri.object<QtJniTypes::UriType>(),
+ sourceParentDocumentUri.object<QtJniTypes::UriType>(),
+ targetParentDocumentUri.object<QtJniTypes::UriType>());
+}
+
+QJniObject renameDocument(const QJniObject &documentUri, const QString &displayName)
+{
+ const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
+ if (!(flags & Document::FLAG_SUPPORTS_RENAME))
+ return {};
+
+ return QJniObject::callStaticMethod<QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::DocumentsContract>(),
+ "renameDocument",
+ contentResolverInstance().object<QtJniTypes::ContentResolverType>(),
+ documentUri.object<QtJniTypes::UriType>(),
+ QJniObject::fromString(displayName).object<jstring>());
+}
+} // End DocumentsContract namespace
+
+// Start of DocumentFile
+
+using namespace DocumentsContract;
+
+namespace {
+class MakeableDocumentFile : public DocumentFile
+{
+public:
+ MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {})
+ : DocumentFile(uri, parent)
+ {}
+};
+}
+
+DocumentFile::DocumentFile(const QJniObject &uri,
+ const DocumentFilePtr &parent)
+ : m_uri{uri}
+ , m_parent{parent}
+{}
+
+QJniObject parseUri(const QString &uri)
+{
+ return QJniObject::callStaticMethod<QtJniTypes::UriType>(
+ QtJniTypes::className<QtJniTypes::Uri>(),
+ "parse",
+ QJniObject::fromString(uri).object<jstring>());
+}
+
+DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName)
+{
+ const QJniObject uri = parseUri(fileName);
+
+ if (DocumentsContract::isDocumentUri(uri))
+ return fromSingleUri(uri);
+
+ const QString documentType = "/document/"_L1;
+ const QString treeType = "/tree/"_L1;
+
+ const int treeIndex = fileName.indexOf(treeType);
+ const int documentIndex = fileName.indexOf(documentType);
+ const int index = fileName.lastIndexOf("/");
+
+ 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<MakeableDocumentFile>(parseUri(fileUrl));
+ if (parentDocFile && parentDocFile->isDirectory())
+ docFile->m_parent = parentDocFile;
+
+ return docFile;
+}
+
+DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri)
+{
+ return std::make_shared<MakeableDocumentFile>(uri);
+}
+
+DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri)
+{
+ QString docId;
+ if (isDocumentUri(treeUri))
+ docId = documentId(treeUri);
+ else
+ docId = treeDocumentId(treeUri);
+
+ return std::make_shared<MakeableDocumentFile>(buildDocumentUriUsingTree(treeUri, docId));
+}
+
+DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName)
+{
+ if (isDirectory()) {
+ return std::make_shared<MakeableDocumentFile>(
+ createDocument(m_uri, mimeType, displayName),
+ shared_from_this());
+ }
+ return {};
+}
+
+DocumentFilePtr DocumentFile::createDirectory(const QString &displayName)
+{
+ if (isDirectory()) {
+ return std::make_shared<MakeableDocumentFile>(
+ createDocument(m_uri, Document::MIME_TYPE_DIR, displayName),
+ shared_from_this());
+ }
+ return {};
+}
+
+const QJniObject &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
+{
+ const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
+ const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
+ m_uri.object<QtJniTypes::UriType>(),
+ FLAG_GRANT_READ_URI_PERMISSION);
+ if (selfUriPermission != 0)
+ return false;
+
+ return !mimeType().isEmpty();
+}
+
+bool DocumentFile::canWrite() const
+{
+ const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
+ const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
+ m_uri.object<QtJniTypes::UriType>(),
+ 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<DocumentFilePtr> DocumentFile::listFiles()
+{
+ std::vector<DocumentFilePtr> 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<MakeableDocumentFile>(uri, shared_from_this()));
+ }
+ return res;
+}
+
+bool DocumentFile::rename(const QString &newName)
+{
+ QJniObject uri;
+ if (newName.startsWith("content://"_L1)) {
+ auto lastSeparatorIndex = [](const QString &file) {
+ int posDecoded = file.lastIndexOf("/");
+ int posEncoded = file.lastIndexOf(QUrl::toPercentEncoding("/"));
+ 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('/'))
+ displayName.remove(0, 1);
+ else if (displayName.startsWith(QUrl::toPercentEncoding("/")))
+ displayName.remove(0, 3);
+
+ uri = renameDocument(m_uri, displayName);
+ } else {
+ // Move
+ QJniObject srcParentUri = fromTreeUri(parseUri(parent))->uri();
+ const QString destParent = newName.left(lastSeparatorIndex(newName));
+ QJniObject targetParentUri = fromTreeUri(parseUri(destParent))->uri();
+ uri = moveDocument(m_uri, srcParentUri, targetParentUri);
+ }
+ } 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 e58c990c51..56d9bae8f7 100644
--- a/src/plugins/platforms/android/androidcontentfileengine.h
+++ b/src/plugins/platforms/android/androidcontentfileengine.h
@@ -5,7 +5,11 @@
#define ANDROIDCONTENTFILEENGINE_H
#include <private/qfsfileengine_p.h>
+
#include <QtCore/qjniobject.h>
+#include <QtCore/qlist.h>
+
+using DocumentFilePtr = std::shared_ptr<class DocumentFile>;
class AndroidContentFileEngine : public QFSFileEngine
{
@@ -14,14 +18,25 @@ public:
bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override;
bool close() override;
qint64 size() const override;
+ bool remove() override;
+ bool rename(const QString &newName) override;
+ bool mkdir(const QString &dirName, bool createParentDirectories,
+ std::optional<QFile::Permissions> permissions = std::nullopt) 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;
- QJniObject m_pfd;
+ void closeNativeFileDescriptor();
+ QString m_initialFile;
+ QJniObject m_pfd;
+ DocumentFilePtr m_documentFile;
};
class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler
@@ -40,9 +55,51 @@ 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;
+ mutable QList<DocumentFilePtr> 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<DocumentFile>
+{
+public:
+ static DocumentFilePtr parseFromAnyUri(const QString &filename);
+ static DocumentFilePtr fromSingleUri(const QJniObject &uri);
+ static DocumentFilePtr fromTreeUri(const QJniObject &treeUri);
+
+ DocumentFilePtr createFile(const QString &mimeType, const QString &displayName);
+ DocumentFilePtr createDirectory(const QString &displayName);
+ const QJniObject &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<DocumentFilePtr> listFiles();
+ bool rename(const QString &newName);
+
+protected:
+ DocumentFile(const QJniObject &uri, const std::shared_ptr<DocumentFile> &parent);
+
+protected:
+ QJniObject m_uri;
+ DocumentFilePtr m_parent;
};
#endif // ANDROIDCONTENTFILEENGINE_H
diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp
index 38b1ed0952..8990289dc4 100644
--- a/src/plugins/platforms/android/androidjniaccessibility.cpp
+++ b/src/plugins/platforms/android/androidjniaccessibility.cpp
@@ -1,6 +1,7 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+#include "androiddeadlockprotector.h"
#include "androidjniaccessibility.h"
#include "androidjnimain.h"
#include "qandroidplatformintegration.h"
@@ -61,6 +62,14 @@ namespace QtAndroidAccessibility
template <typename Func, typename Ret>
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);
@@ -131,6 +140,11 @@ namespace QtAndroidAccessibility
QtAndroid::notifyValueChanged(accessibilityObjectId, value);
}
+ void notifyScrolledEvent(uint accessiblityObjectId)
+ {
+ QtAndroid::notifyScrolledEvent(accessiblityObjectId);
+ }
+
static QVarLengthArray<int, 8> childIdListForAccessibleObject_helper(int objectId)
{
QAccessibleInterface *iface = interfaceFromId(objectId);
@@ -188,7 +202,7 @@ namespace QtAndroidAccessibility
return result;
}
- static QRect screenRect_helper(int objectId)
+ static QRect screenRect_helper(int objectId, bool clip = true)
{
QRect rect;
QAccessibleInterface *iface = interfaceFromId(objectId);
@@ -196,7 +210,7 @@ namespace QtAndroidAccessibility
rect = QHighDpi::toNativePixels(iface->rect(), iface->window());
}
// If the widget is not fully in-bound in its parent then we have to clip the rectangle to draw
- if (iface && iface->parent() && iface->parent()->isValid()) {
+ if (clip && iface && iface->parent() && iface->parent()->isValid()) {
const auto parentRect = QHighDpi::toNativePixels(iface->parent()->rect(), iface->parent()->window());
rect = rect.intersected(parentRect);
}
@@ -298,23 +312,43 @@ namespace QtAndroidAccessibility
static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
{
bool result = false;
+
+ const auto& ids = childIdListForAccessibleObject_helper(objectId);
+ if (ids.isEmpty())
+ return false;
+
+ const int firstChildId = ids.first();
+ const QRect oldPosition = screenRect_helper(firstChildId, false);
+
if (m_accessibilityContext) {
runInObjectContext(m_accessibilityContext, [objectId]() {
return scroll_helper(objectId, QAccessibleActionInterface::increaseAction());
}, &result);
}
- return result;
+
+ // Don't check for position change if the call was not successful
+ return result && oldPosition != screenRect_helper(firstChildId, false);
}
static jboolean scrollBackward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId)
{
bool result = false;
+
+ const auto& ids = childIdListForAccessibleObject_helper(objectId);
+ if (ids.isEmpty())
+ return false;
+
+ const int firstChildId = ids.first();
+ const QRect oldPosition = screenRect_helper(firstChildId, false);
+
if (m_accessibilityContext) {
runInObjectContext(m_accessibilityContext, [objectId]() {
return scroll_helper(objectId, QAccessibleActionInterface::decreaseAction());
}, &result);
}
- return result;
+
+ // Don't check for position change if the call was not successful
+ return result && oldPosition != screenRect_helper(firstChildId, false);
}
diff --git a/src/plugins/platforms/android/androidjniaccessibility.h b/src/plugins/platforms/android/androidjniaccessibility.h
index 94e64762e4..9bbbe80fe9 100644
--- a/src/plugins/platforms/android/androidjniaccessibility.h
+++ b/src/plugins/platforms/android/androidjniaccessibility.h
@@ -19,6 +19,7 @@ namespace QtAndroidAccessibility
void notifyObjectHide(uint accessibilityObjectId);
void notifyObjectFocus(uint accessibilityObjectId);
void notifyValueChanged(uint accessibilityObjectId);
+ void notifyScrolledEvent(uint accessibilityObjectId);
void createAccessibilityContextObject(QObject *parent);
}
diff --git a/src/plugins/platforms/android/androidjniclipboard.cpp b/src/plugins/platforms/android/androidjniclipboard.cpp
index 7b2c2c0443..81663cac0c 100644
--- a/src/plugins/platforms/android/androidjniclipboard.cpp
+++ b/src/plugins/platforms/android/androidjniclipboard.cpp
@@ -35,27 +35,25 @@ namespace QtAndroidClipboard
void setClipboardMimeData(QMimeData *data)
{
clearClipboardData();
- if (data->hasText()) {
+ if (data->hasUrls()) {
+ QList<QUrl> urls = data->urls();
+ for (const auto &u : std::as_const(urls)) {
+ QJniObject::callStaticMethod<void>(applicationClass(),
+ "setClipboardUri",
+ "(Ljava/lang/String;)V",
+ QJniObject::fromString(u.toEncoded()).object());
+ }
+ } else if (data->hasText()) { // hasText || hasUrls, so the order matter here.
QJniObject::callStaticMethod<void>(applicationClass(),
"setClipboardText", "(Ljava/lang/String;)V",
QJniObject::fromString(data->text()).object());
- }
- if (data->hasHtml()) {
+ } else if (data->hasHtml()) {
QJniObject::callStaticMethod<void>(applicationClass(),
"setClipboardHtml",
"(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(data->text()).object(),
QJniObject::fromString(data->html()).object());
}
- if (data->hasUrls()) {
- QList<QUrl> urls = data->urls();
- for (const auto &u : qAsConst(urls)) {
- QJniObject::callStaticMethod<void>(applicationClass(),
- "setClipboardUri",
- "(Ljava/lang/String;)V",
- QJniObject::fromString(u.toEncoded()).object());
- }
- }
}
QMimeData *getClipboardMimeData()
diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp
index 3b5e656630..a7300484f2 100644
--- a/src/plugins/platforms/android/androidjnimain.cpp
+++ b/src/plugins/platforms/android/androidjnimain.cpp
@@ -27,6 +27,7 @@
#include <QtCore/qbasicatomic.h>
#include <QtCore/qjnienvironment.h>
#include <QtCore/qjniobject.h>
+#include <QtCore/qprocess.h>
#include <QtCore/qresource.h>
#include <QtCore/qthread.h>
#include <QtGui/private/qguiapplication_p.h>
@@ -197,6 +198,12 @@ namespace QtAndroid
"(ILjava/lang/String;)V", accessibilityObjectId, value);
}
+ void notifyScrolledEvent(uint accessibilityObjectId)
+ {
+ QJniObject::callStaticMethod<void>(m_applicationClass, "notifyScrolledEvent", "(I)V",
+ accessibilityObjectId);
+ }
+
void notifyQtAndroidPluginRunning(bool running)
{
QJniObject::callStaticMethod<void>(m_applicationClass, "notifyQtAndroidPluginRunning","(Z)V", running);
@@ -445,11 +452,11 @@ static jboolean startQtAndroidPlugin(JNIEnv *env, jobject /*object*/, jstring pa
m_mainLibraryHnd = nullptr;
const char *nativeString = env->GetStringUTFChars(paramsString, 0);
- QByteArray string = nativeString;
+ const QStringList argsList = QProcess::splitCommand(QString::fromUtf8(nativeString));
env->ReleaseStringUTFChars(paramsString, nativeString);
- for (auto str : string.split('\t'))
- m_applicationParams.append(str.split(' '));
+ for (const QString &arg : argsList)
+ m_applicationParams.append(arg.toUtf8());
// Go home
QDir::setCurrent(QDir::homePath());
@@ -614,24 +621,27 @@ static void setDisplayMetrics(JNIEnv * /*env*/, jclass /*clazz*/, jint screenWid
jint availableHeightPixels, jdouble xdpi, jdouble ydpi,
jdouble scaledDensity, jdouble density, jfloat refreshRate)
{
+ Q_UNUSED(availableLeftPixels)
+ Q_UNUSED(availableTopPixels)
+
m_availableWidthPixels = availableWidthPixels;
m_availableHeightPixels = availableHeightPixels;
m_scaledDensity = scaledDensity;
m_density = density;
+ const QSize screenSize(screenWidthPixels, screenHeightPixels);
+ // available geometry always starts from top left
+ const QRect availableGeometry(0, 0, availableWidthPixels, availableHeightPixels);
+ const QSize physicalSize(qRound(double(screenWidthPixels) / xdpi * 25.4),
+ qRound(double(screenHeightPixels) / ydpi * 25.4));
+
QMutexLocker lock(&m_platformMutex);
if (!m_androidPlatformIntegration) {
QAndroidPlatformIntegration::setDefaultDisplayMetrics(
- availableLeftPixels, availableTopPixels, availableWidthPixels,
- availableHeightPixels, qRound(double(screenWidthPixels) / xdpi * 25.4),
- qRound(double(screenHeightPixels) / ydpi * 25.4), screenWidthPixels,
- screenHeightPixels);
+ availableGeometry.left(), availableGeometry.top(), availableGeometry.width(),
+ availableGeometry.height(), physicalSize.width(), physicalSize.height(),
+ screenSize.width(), screenSize.height());
} else {
- const QSize physicalSize(qRound(double(screenWidthPixels) / xdpi * 25.4),
- qRound(double(screenHeightPixels) / ydpi * 25.4));
- const QSize screenSize(screenWidthPixels, screenHeightPixels);
- const QRect availableGeometry(availableLeftPixels, availableTopPixels,
- availableWidthPixels, availableHeightPixels);
m_androidPlatformIntegration->setScreenSizeParameters(physicalSize, screenSize,
availableGeometry);
m_androidPlatformIntegration->setRefreshRate(refreshRate);
@@ -745,6 +755,12 @@ static void handleRefreshRateChanged(JNIEnv */*env*/, jclass /*cls*/, jfloat ref
m_androidPlatformIntegration->setRefreshRate(refreshRate);
}
+static void handleUiDarkModeChanged(JNIEnv */*env*/, jobject /*thiz*/, jint newUiMode)
+{
+ QAndroidPlatformIntegration::setAppearance(
+ (newUiMode == 1 ) ? QPlatformTheme::Appearance::Dark : QPlatformTheme::Appearance::Light);
+}
+
static void onActivityResult(JNIEnv */*env*/, jclass /*cls*/,
jint requestCode,
jint resultCode,
@@ -774,6 +790,7 @@ static JNINativeMethod methods[] = {
{ "setSurface", "(ILjava/lang/Object;II)V", (void *)setSurface },
{ "updateWindow", "()V", (void *)updateWindow },
{ "updateApplicationState", "(I)V", (void *)updateApplicationState },
+ { "handleUiDarkModeChanged", "(I)V", (void *)handleUiDarkModeChanged },
{ "handleOrientationChanged", "(II)V", (void *)handleOrientationChanged },
{ "onActivityResult", "(IILandroid/content/Intent;)V", (void *)onActivityResult },
{ "onNewIntent", "(Landroid/content/Intent;)V", (void *)onNewIntent },
diff --git a/src/plugins/platforms/android/androidjnimain.h b/src/plugins/platforms/android/androidjnimain.h
index 8d05e31f66..4879d7e5b2 100644
--- a/src/plugins/platforms/android/androidjnimain.h
+++ b/src/plugins/platforms/android/androidjnimain.h
@@ -69,6 +69,7 @@ namespace QtAndroid
void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId);
void notifyObjectFocus(uint accessibilityObjectId);
void notifyValueChanged(uint accessibilityObjectId, jstring value);
+ void notifyScrolledEvent(uint accessibilityObjectId);
void notifyQtAndroidPluginRunning(bool running);
const char *classErrorMsgFmt();
diff --git a/src/plugins/platforms/android/androidjnimenu.cpp b/src/plugins/platforms/android/androidjnimenu.cpp
index 7b0091d277..7f294f5316 100644
--- a/src/plugins/platforms/android/androidjnimenu.cpp
+++ b/src/plugins/platforms/android/androidjnimenu.cpp
@@ -117,7 +117,7 @@ namespace QtAndroidMenu
visibleMenuBar = 0;
activeTopLevelWindow = window;
- for (QAndroidPlatformMenuBar *menuBar : qAsConst(menuBars)) {
+ for (QAndroidPlatformMenuBar *menuBar : std::as_const(menuBars)) {
if (menuBar->parentWindow() == window) {
visibleMenuBar = menuBar;
resetMenuBar();
@@ -318,7 +318,7 @@ namespace QtAndroidMenu
item->activated();
visibleMenu->aboutToHide();
visibleMenu = 0;
- for (QAndroidPlatformMenu *menu : qAsConst(pendingContextMenus)) {
+ for (QAndroidPlatformMenu *menu : std::as_const(pendingContextMenus)) {
if (menu->isVisible())
menu->aboutToHide();
}
diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp
index 36fa2dd945..43cc36d222 100644
--- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp
+++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp
@@ -108,6 +108,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.
QJniObject files = QJniObject::callStaticObjectMethod(QtAndroid::applicationClass(),
"listAssetContent",
"(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;",
@@ -186,7 +188,7 @@ public:
return m_currentIterator->currentFileName();
}
- virtual QString currentFilePath() const
+ QString currentFilePath() const override
{
if (!m_currentIterator)
return {};
@@ -350,8 +352,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);
}
}
diff --git a/src/plugins/platforms/android/qandroideventdispatcher.cpp b/src/plugins/platforms/android/qandroideventdispatcher.cpp
index 238addee58..9c3022504d 100644
--- a/src/plugins/platforms/android/qandroideventdispatcher.cpp
+++ b/src/plugins/platforms/android/qandroideventdispatcher.cpp
@@ -75,7 +75,7 @@ void QAndroidEventDispatcherStopper::startAll()
if (!m_started.testAndSetOrdered(0, 1))
return;
- for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers))
+ for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers))
d->start();
}
@@ -85,7 +85,7 @@ void QAndroidEventDispatcherStopper::stopAll()
if (!m_started.testAndSetOrdered(1, 0))
return;
- for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers))
+ for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers))
d->stop();
}
@@ -104,6 +104,6 @@ void QAndroidEventDispatcherStopper::removeEventDispatcher(QAndroidEventDispatch
void QAndroidEventDispatcherStopper::goingToStop(bool stop)
{
QMutexLocker lock(&m_mutex);
- for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers))
+ for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers))
d->goingToStop(stop);
}
diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp
index 33eafd54aa..61fc21a6bc 100644
--- a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp
+++ b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp
@@ -32,6 +32,8 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *
QtAndroidAccessibility::notifyObjectFocus(event->uniqueId());
} else if (event->type() == QAccessible::ValueChanged) {
QtAndroidAccessibility::notifyValueChanged(event->uniqueId());
+ } else if (event->type() == QAccessible::ScrollingEnd) {
+ QtAndroidAccessibility::notifyScrolledEvent(event->uniqueId());
}
}
diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp
index 6d28bd2388..3723e33371 100644
--- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp
+++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp
@@ -97,6 +97,22 @@ void QAndroidPlatformFileDialogHelper::setInitialFileName(const QString &title)
extraTitle.object(), QJniObject::fromString(title).object());
}
+void QAndroidPlatformFileDialogHelper::setInitialDirectoryUri(const QString &directory)
+{
+ if (directory.isEmpty())
+ return;
+
+ if (QNativeInterface::QAndroidApplication::sdkVersion() < 26)
+ return;
+
+ const auto extraInitialUri = QJniObject::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(),
+ QJniObject::fromString(directory).object());
+}
+
void QAndroidPlatformFileDialogHelper::setOpenableCategory()
{
const QJniObject CATEGORY_OPENABLE = QJniObject::getStaticObjectField(
@@ -179,11 +195,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win
if (options()->acceptMode() == QFileDialogOptions::AcceptSave) {
m_intent = getFileDialogIntent("ACTION_CREATE_DOCUMENT");
const QList<QUrl> 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:
@@ -207,6 +220,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win
setMimeTypes();
}
+ setInitialDirectoryUri(m_directory.toString());
+
QtAndroidPrivate::registerActivityResultListener(this);
m_activity.callMethod<void>("startActivityForResult", "(Landroid/content/Intent;I)V",
m_intent.object(), REQUEST_CODE);
@@ -220,6 +235,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 4281cb8198..156eda9142 100644
--- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h
+++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h
@@ -32,8 +32,8 @@ public:
void setFilter() override {}
QList<QUrl> selectedFiles() const override { return m_selectedFile; }
void selectFile(const QUrl &) override {}
- QUrl directory() const override { return QUrl(); }
- void setDirectory(const QUrl &) override {}
+ 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;
@@ -41,12 +41,14 @@ private:
QJniObject getFileDialogIntent(const QString &intentType);
void takePersistableUriPermission(const QJniObject &uri);
void setInitialFileName(const QString &title);
+ void setInitialDirectoryUri(const QString &directory);
void setOpenableCategory();
void setAllowMultipleSelections(bool allowMultiple);
void setMimeTypes();
QEventLoop m_eventLoop;
QList<QUrl> m_selectedFile;
+ QUrl m_directory;
QJniObject m_intent;
const QJniObject m_activity;
};
diff --git a/src/plugins/platforms/android/qandroidplatformintegration.cpp b/src/plugins/platforms/android/qandroidplatformintegration.cpp
index 19a7326115..a52dcdfc15 100644
--- a/src/plugins/platforms/android/qandroidplatformintegration.cpp
+++ b/src/plugins/platforms/android/qandroidplatformintegration.cpp
@@ -432,7 +432,7 @@ QStringList QAndroidPlatformIntegration::themeNames() const
QPlatformTheme *QAndroidPlatformIntegration::createPlatformTheme(const QString &name) const
{
if (androidThemeName == name)
- return new QAndroidPlatformTheme(m_androidPlatformNativeInterface);
+ return QAndroidPlatformTheme::instance(m_androidPlatformNativeInterface);
return 0;
}
@@ -488,6 +488,18 @@ void QAndroidPlatformIntegration::setScreenSize(int width, int height)
QMetaObject::invokeMethod(m_primaryScreen, "setSize", Qt::AutoConnection, Q_ARG(QSize, QSize(width, height)));
}
+QPlatformTheme::Appearance QAndroidPlatformIntegration::m_appearance = QPlatformTheme::Appearance::Light;
+
+void QAndroidPlatformIntegration::setAppearance(QPlatformTheme::Appearance newAppearance)
+{
+ if (m_appearance == newAppearance)
+ return;
+ m_appearance = newAppearance;
+
+ QMetaObject::invokeMethod(qGuiApp,
+ [] () { QAndroidPlatformTheme::instance()->updateAppearance();});
+}
+
void QAndroidPlatformIntegration::setScreenSizeParameters(const QSize &physicalSize,
const QSize &screenSize,
const QRect &availableGeometry)
diff --git a/src/plugins/platforms/android/qandroidplatformintegration.h b/src/plugins/platforms/android/qandroidplatformintegration.h
index 6e87c9c02b..1b26710d73 100644
--- a/src/plugins/platforms/android/qandroidplatformintegration.h
+++ b/src/plugins/platforms/android/qandroidplatformintegration.h
@@ -13,6 +13,7 @@
#include <qpa/qplatformnativeinterface.h>
#include <qpa/qplatformopenglcontext.h>
#include <qpa/qplatformoffscreensurface.h>
+#include <qpa/qplatformtheme.h>
#include <EGL/egl.h>
#include <memory>
@@ -103,6 +104,8 @@ public:
void flushPendingUpdates();
+ static void setAppearance(QPlatformTheme::Appearance newAppearance);
+ static QPlatformTheme::Appearance appearance() { return m_appearance; }
#if QT_CONFIG(vulkan)
QPlatformVulkanInstance *createPlatformVulkanInstance(QVulkanInstance *instance) const override;
#endif
@@ -115,6 +118,8 @@ private:
QThread *m_mainThread;
+ static QPlatformTheme::Appearance m_appearance;
+
static QRect m_defaultAvailableGeometry;
static QSize m_defaultPhysicalSize;
static QSize m_defaultScreenSize;
diff --git a/src/plugins/platforms/android/qandroidplatformscreen.cpp b/src/plugins/platforms/android/qandroidplatformscreen.cpp
index d11b7b197b..920a2d19f8 100644
--- a/src/plugins/platforms/android/qandroidplatformscreen.cpp
+++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp
@@ -312,7 +312,7 @@ void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect)
void QAndroidPlatformScreen::applicationStateChanged(Qt::ApplicationState state)
{
- for (QAndroidPlatformWindow *w : qAsConst(m_windowStack))
+ for (QAndroidPlatformWindow *w : std::as_const(m_windowStack))
w->applicationStateChanged(state);
if (state <= Qt::ApplicationHidden) {
@@ -353,7 +353,7 @@ void QAndroidPlatformScreen::doRedraw(QImage* screenGrabImage)
// windows that have renderToTexture children (i.e. they need the OpenGL path) then
// we do not need an overlay surface.
bool hasVisibleRasterWindows = false;
- for (QAndroidPlatformWindow *window : qAsConst(m_windowStack)) {
+ for (QAndroidPlatformWindow *window : std::as_const(m_windowStack)) {
if (window->window()->isVisible() && window->isRaster() && !qt_window_private(window->window())->compositing) {
hasVisibleRasterWindows = true;
break;
@@ -408,7 +408,7 @@ void QAndroidPlatformScreen::doRedraw(QImage* screenGrabImage)
compositePainter.setCompositionMode(QPainter::CompositionMode_Source);
QRegion visibleRegion(m_dirtyRect);
- for (QAndroidPlatformWindow *window : qAsConst(m_windowStack)) {
+ for (QAndroidPlatformWindow *window : std::as_const(m_windowStack)) {
if (!window->window()->isVisible()
|| qt_window_private(window->window())->compositing
|| !window->isRaster())
@@ -466,7 +466,7 @@ static const int androidLogicalDpi = 72;
QDpi QAndroidPlatformScreen::logicalDpi() const
{
- qreal lDpi = QtAndroid::scaledDensity() * androidLogicalDpi;
+ qreal lDpi = QtAndroid::pixelDensity() * androidLogicalDpi;
return QDpi(lDpi, lDpi);
}
diff --git a/src/plugins/platforms/android/qandroidplatformservices.cpp b/src/plugins/platforms/android/qandroidplatformservices.cpp
index 8f8f702011..2599fe6db2 100644
--- a/src/plugins/platforms/android/qandroidplatformservices.cpp
+++ b/src/plugins/platforms/android/qandroidplatformservices.cpp
@@ -47,12 +47,13 @@ bool QAndroidPlatformServices::openUrl(const QUrl &theUrl)
// if the file is local, we need to pass the MIME type, otherwise Android
// does not start an Intent to view this file
const auto fileScheme = "file"_L1;
- if ((url.scheme().isEmpty() || url.scheme() == fileScheme) && QFile::exists(url.path())) {
- // a real URL including the scheme is needed, else the Intent can not be started
+
+ // a real URL including the scheme is needed, else the Intent can not be started
+ if (url.scheme().isEmpty())
url.setScheme(fileScheme);
- QMimeDatabase mimeDb;
- mime = mimeDb.mimeTypeForUrl(url).name();
- }
+
+ if (url.scheme() == fileScheme)
+ mime = QMimeDatabase().mimeTypeForUrl(url).name();
using namespace QNativeInterface;
QJniObject urlString = QJniObject::fromString(url.toString());
diff --git a/src/plugins/platforms/android/qandroidplatformtheme.cpp b/src/plugins/platforms/android/qandroidplatformtheme.cpp
index c5b9ba9dee..6eba33df45 100644
--- a/src/plugins/platforms/android/qandroidplatformtheme.cpp
+++ b/src/plugins/platforms/android/qandroidplatformtheme.cpp
@@ -158,6 +158,9 @@ QJsonObject AndroidStyle::loadStyleData()
if (!stylePath.isEmpty() && !stylePath.endsWith(slashChar))
stylePath += slashChar;
+ if (QAndroidPlatformIntegration::appearance() == QPlatformTheme::Appearance::Dark)
+ stylePath += "darkUiMode/"_L1;
+
Q_ASSERT(!stylePath.isEmpty());
QString androidTheme = QLatin1StringView(qgetenv("QT_ANDROID_THEME"));
@@ -185,13 +188,22 @@ QJsonObject AndroidStyle::loadStyleData()
return document.object();
}
-static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette)
+static void loadAndroidStyle(QPalette *defaultPalette, std::shared_ptr<AndroidStyle> &style)
{
double pixelDensity = QHighDpiScaling::isActive() ? QtAndroid::pixelDensity() : 1.0;
- std::shared_ptr<AndroidStyle> style = std::make_shared<AndroidStyle>();
+ if (style) {
+ style->m_standardPalette = QPalette();
+ style->m_palettes.clear();
+ style->m_fonts.clear();
+ style->m_QWidgetsFonts.clear();
+ } else {
+ style = std::make_shared<AndroidStyle>();
+ }
+
style->m_styleData = AndroidStyle::loadStyleData();
+
if (style->m_styleData.isEmpty())
- return std::shared_ptr<AndroidStyle>();
+ return;
{
QFont font("Droid Sans Mono"_L1, 14.0 * 100 / 72);
@@ -293,11 +305,43 @@ static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette)
// Extract palette information
}
}
- return style;
+}
+
+QAndroidPlatformTheme *QAndroidPlatformTheme::m_instance = nullptr;
+
+QAndroidPlatformTheme *QAndroidPlatformTheme::instance(
+ QAndroidPlatformNativeInterface *androidPlatformNativeInterface)
+{
+ if (androidPlatformNativeInterface && !m_instance) {
+ m_instance = new QAndroidPlatformTheme(androidPlatformNativeInterface);
+ }
+ return m_instance;
}
QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *androidPlatformNativeInterface)
{
+ updateStyle();
+
+ androidPlatformNativeInterface->m_androidStyle = m_androidStyleData;
+
+ // default in case the style has not set a font
+ m_systemFont = QFont("Roboto"_L1, 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi
+}
+
+QAndroidPlatformTheme::~QAndroidPlatformTheme()
+{
+ m_instance = nullptr;
+}
+
+void QAndroidPlatformTheme::updateAppearance()
+{
+ updateStyle();
+ QWindowSystemInterface::handleThemeChange();
+}
+
+void QAndroidPlatformTheme::updateStyle()
+{
+ QColor windowText = Qt::black;
QColor background(229, 229, 229);
QColor light = background.lighter(150);
QColor mid(background.darker(130));
@@ -314,7 +358,27 @@ QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *an
QColor highlight(148, 210, 231);
QColor disabledShadow = shadow.lighter(150);
- m_defaultPalette = QPalette(Qt::black,background,light,dark,mid,text,base);
+ if (appearance() == QPlatformTheme::Appearance::Dark) {
+ // Colors were prepared based on Theme.DeviceDefault.DayNight
+ windowText = QColor(250, 250, 250);
+ background = QColor(48, 48, 48);
+ light = background.darker(150);
+ mid = background.lighter(130);
+ midLight = mid.darker(110);
+ base = background;
+ disabledBase = background;
+ dark = background.darker(150);
+ darkDisabled = dark.darker(110);
+ text = QColor(250, 250, 250);
+ highlightedText = QColor(250, 250, 250);
+ disabledText = QColor(96, 96, 96);
+ button = QColor(48, 48, 48);
+ shadow = QColor(32, 32, 32);
+ highlight = QColor(102, 178, 204);
+ disabledShadow = shadow.darker(150);
+ }
+
+ m_defaultPalette = QPalette(windowText,background,light,dark,mid,text,base);
m_defaultPalette.setBrush(QPalette::Midlight, midLight);
m_defaultPalette.setBrush(QPalette::Button, button);
m_defaultPalette.setBrush(QPalette::Shadow, shadow);
@@ -330,12 +394,8 @@ QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *an
m_defaultPalette.setBrush(QPalette::Active, QPalette::Highlight, highlight);
m_defaultPalette.setBrush(QPalette::Inactive, QPalette::Highlight, highlight);
m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Highlight, highlight.lighter(150));
- m_androidStyleData = loadAndroidStyle(&m_defaultPalette);
- QGuiApplication::setPalette(m_defaultPalette);
- androidPlatformNativeInterface->m_androidStyle = m_androidStyleData;
- // default in case the style has not set a font
- m_systemFont = QFont("Roboto"_L1, 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi
+ loadAndroidStyle(&m_defaultPalette, m_androidStyleData);
}
QPlatformMenuBar *QAndroidPlatformTheme::createPlatformMenuBar() const
@@ -358,6 +418,11 @@ void QAndroidPlatformTheme::showPlatformMenuBar()
QtAndroidMenu::openOptionsMenu();
}
+QPlatformTheme::Appearance QAndroidPlatformTheme::appearance() const
+{
+ return QAndroidPlatformIntegration::appearance();
+}
+
static inline int paletteType(QPlatformTheme::Palette type)
{
switch (type) {
diff --git a/src/plugins/platforms/android/qandroidplatformtheme.h b/src/plugins/platforms/android/qandroidplatformtheme.h
index ec39ed4794..43f6b44a3f 100644
--- a/src/plugins/platforms/android/qandroidplatformtheme.h
+++ b/src/plugins/platforms/android/qandroidplatformtheme.h
@@ -30,11 +30,14 @@ class QAndroidPlatformNativeInterface;
class QAndroidPlatformTheme: public QPlatformTheme
{
public:
- QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface);
+ ~QAndroidPlatformTheme();
+ void updateAppearance();
+ void updateStyle();
QPlatformMenuBar *createPlatformMenuBar() const override;
QPlatformMenu *createPlatformMenu() const override;
QPlatformMenuItem *createPlatformMenuItem() const override;
void showPlatformMenuBar() override;
+ Appearance appearance() const override;
const QPalette *palette(Palette type = SystemPalette) const override;
const QFont *font(Font type = SystemFont) const override;
QVariant themeHint(ThemeHint hint) const override;
@@ -42,8 +45,12 @@ public:
bool usePlatformNativeDialog(DialogType type) const override;
QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override;
+ static QAndroidPlatformTheme *instance(
+ QAndroidPlatformNativeInterface * androidPlatformNativeInterface = nullptr);
private:
+ QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface);
+ static QAndroidPlatformTheme * m_instance;
std::shared_ptr<AndroidStyle> m_androidStyleData;
QPalette m_defaultPalette;
QFont m_systemFont;