diff --git a/src/plugins/platforms/android/CMakeLists.txt b/src/plugins/platforms/android/CMakeLists.txt index 40def7ba77f..db961e7a938 100644 --- a/src/plugins/platforms/android/CMakeLists.txt +++ b/src/plugins/platforms/android/CMakeLists.txt @@ -29,6 +29,7 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin qandroidplatformfiledialoghelper.cpp qandroidplatformfiledialoghelper.h qandroidplatformfontdatabase.cpp qandroidplatformfontdatabase.h qandroidplatformforeignwindow.cpp qandroidplatformforeignwindow.h + qandroidplatformiconengine.cpp qandroidplatformiconengine.h qandroidplatformintegration.cpp qandroidplatformintegration.h qandroidplatformmenu.cpp qandroidplatformmenu.h qandroidplatformmenubar.cpp qandroidplatformmenubar.h diff --git a/src/plugins/platforms/android/qandroidplatformiconengine.cpp b/src/plugins/platforms/android/qandroidplatformiconengine.cpp new file mode 100644 index 00000000000..5c07de23195 --- /dev/null +++ b/src/plugins/platforms/android/qandroidplatformiconengine.cpp @@ -0,0 +1,357 @@ +// Copyright (C) 2023 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 "qandroidplatformiconengine.h" +#include "androidjnimain.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +Q_LOGGING_CATEGORY(lcIconEngineFontDownload, "qt.qpa.iconengine.fontdownload") + +// the primary types to work with the FontRequest API +Q_DECLARE_JNI_CLASS(FontRequest, "androidx/core/provider/FontRequest") +Q_DECLARE_JNI_CLASS(FontsContractCompat, "androidx/core/provider/FontsContractCompat") +Q_DECLARE_JNI_CLASS(FontFamilyResult, "androidx/core/provider/FontsContractCompat$FontFamilyResult") +Q_DECLARE_JNI_CLASS(FontInfo, "androidx/core/provider/FontsContractCompat$FontInfo") + +// various utility types +Q_DECLARE_JNI_CLASS(List, "java/util/List"); // List is just an Interface +Q_DECLARE_JNI_CLASS(ArrayList, "java/util/ArrayList"); +Q_DECLARE_JNI_CLASS(HashSet, "java/util/HashSet"); +Q_DECLARE_JNI_CLASS(Uri, "android/net/Uri") +Q_DECLARE_JNI_CLASS(CancellationSignal, "android/os/CancellationSignal") +Q_DECLARE_JNI_CLASS(ParcelFileDescriptor, "android/os/ParcelFileDescriptor") +Q_DECLARE_JNI_CLASS(ContentResolver, "android/content/ContentResolver") +Q_DECLARE_JNI_CLASS(PackageManager, "android/content/pm/PackageManager") +Q_DECLARE_JNI_CLASS(ProviderInfo, "android/content/pm/ProviderInfo") +Q_DECLARE_JNI_CLASS(PackageInfo, "android/content/pm/PackageInfo") +Q_DECLARE_JNI_CLASS(Signature, "android/content/pm/Signature") + +namespace FontProvider { + +static QString fetchFont(const QString &query) +{ + using namespace QtJniTypes; + + static QMap triedFonts; + const auto it = triedFonts.find(query); + if (it != triedFonts.constEnd()) + return it.value(); + + QString fontFamily; + triedFonts[query] = fontFamily; // mark as tried + + QStringList loadedFamilies; + if (QFile file(query); file.open(QIODevice::ReadOnly)) { + qCDebug(lcIconEngineFontDownload) << "Loading font from resource" << query; + const QByteArray fontData = file.readAll(); + int fontId = QFontDatabase::addApplicationFontFromData(fontData); + loadedFamilies << QFontDatabase::applicationFontFamilies(fontId); + } else { + const QString package = u"com.google.android.gms"_s; + const QString authority = u"com.google.android.gms.fonts"_s; + + // First we access the content provider to get the signatures of the authority for the package + const auto context = QtAndroidPrivate::context(); + + auto packageManager = context.callMethod("getPackageManager"); + if (!packageManager.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to instantiate PackageManager"); + return fontFamily; + } + const int signaturesField = PackageManager::getStaticField("GET_SIGNATURES"); + auto providerInfo = packageManager.callMethod("resolveContentProvider", + authority, 0); + if (!providerInfo.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to resolve content provider"); + return fontFamily; + } + const QString packageName = providerInfo.getField("packageName"); + if (packageName != package) { + qCWarning(lcIconEngineFontDownload, "Mismatched provider package - expected '%s', got '%s'", + package.toUtf8().constData(), packageName.toUtf8().constData()); + return fontFamily; + } + auto packageInfo = packageManager.callMethod("getPackageInfo", + package, signaturesField); + if (!packageInfo.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to get package info with signature field %d", + signaturesField); + return fontFamily; + } + const auto signatures = packageInfo.getField("signatures"); + if (!signatures.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to get signature array from package info"); + return fontFamily; + } + + // FontRequest wants a list of sets for the certificates + ArrayList outerList; + HashSet innerSet; + Q_ASSERT(outerList.isValid() && innerSet.isValid()); + + for (QJniObject signature : signatures) { + const QJniArray byteArray = signature.callMethod("toByteArray"); + + // add takes an Object, not an Array + if (!innerSet.callMethod("add", byteArray.object())) + qCWarning(lcIconEngineFontDownload, "Failed to add signature to set"); + } + // Add the set to the list + if (!outerList.callMethod("add", innerSet.object())) + qCWarning(lcIconEngineFontDownload, "Failed to add set to certificate list"); + + // FontRequest constructor wants a List interface, not an ArrayList + FontRequest fontRequest(authority, package, query, outerList.object()); + if (!fontRequest.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to create font request for '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + // Call FontsContractCompat::fetchFonts with the FontRequest object + auto fontFamilyResult = FontsContractCompat::callStaticMethod( + "fetchFonts", + context, + CancellationSignal(nullptr), + fontRequest); + if (!fontFamilyResult.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to fetch fonts for query '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + enum class StatusCode { + OK = 0, + UNEXPECTED_DATA_PROVIDED = 1, + WRONG_CERTIFICATES = 2, + }; + + const StatusCode statusCode = fontFamilyResult.callMethod("getStatusCode"); + switch (statusCode) { + case StatusCode::OK: + break; + case StatusCode::UNEXPECTED_DATA_PROVIDED: + qCWarning(lcIconEngineFontDownload, "Provider returned unexpected data for query '%s'", + query.toUtf8().constData()); + return fontFamily; + case StatusCode::WRONG_CERTIFICATES: + qCWarning(lcIconEngineFontDownload, "Wrong Certificates provided in query '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + const auto fontInfos = fontFamilyResult.callMethod("getFonts"); + if (!fontInfos.isValid()) { + qCWarning(lcIconEngineFontDownload, "FontFamilyResult::getFonts returned null object for '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + auto contentResolver = context.callMethod("getContentResolver"); + + for (QJniObject fontInfo : fontInfos) { + if (!fontInfo.isValid()) { + qCDebug(lcIconEngineFontDownload, "Received null-fontInfo object, skipping"); + continue; + } + enum class ResultCode { + OK = 0, + FONT_NOT_FOUND = 1, + FONT_UNAVAILABLE = 2, + MALFORMED_QUERY = 3, + }; + const ResultCode resultCode = fontInfo.callMethod("getResultCode"); + switch (resultCode) { + case ResultCode::OK: + break; + case ResultCode::FONT_NOT_FOUND: + qCWarning(lcIconEngineFontDownload, "Font '%s' could not be found", + query.toUtf8().constData()); + return fontFamily; + case ResultCode::FONT_UNAVAILABLE: + qCWarning(lcIconEngineFontDownload, "Font '%s' is unavailable at", + query.toUtf8().constData()); + return fontFamily; + case ResultCode::MALFORMED_QUERY: + qCWarning(lcIconEngineFontDownload, "Query string '%s' is malformed", + query.toUtf8().constData()); + return fontFamily; + } + auto fontUri = fontInfo.callMethod("getUri"); + // in this case the Font URI is always a content scheme file, made + // so the app requesting it has permissions to open + auto fileDescriptor = contentResolver.callMethod("openFileDescriptor", + fontUri, u"r"_s); + if (!fileDescriptor.isValid()) { + qCWarning(lcIconEngineFontDownload, "Font file '%s' not accessible", + fontUri.toString().toUtf8().constData()); + continue; + } + + int fd = fileDescriptor.callMethod("detachFd"); + QFile file; + file.open(fd, QFile::OpenModeFlag::ReadOnly, QFile::FileHandleFlag::AutoCloseHandle); + const QByteArray fontData = file.readAll(); + qCDebug(lcIconEngineFontDownload) << "Font file read:" << fontData.size() << "bytes"; + int fontId = QFontDatabase::addApplicationFontFromData(fontData); + loadedFamilies << QFontDatabase::applicationFontFamilies(fontId); + } + } + + qCDebug(lcIconEngineFontDownload) << "Query '" << query << "' added families" << loadedFamilies; + if (!loadedFamilies.isEmpty()) + fontFamily = loadedFamilies.first(); + triedFonts[query] = fontFamily; + return fontFamily; +} +} + +QAndroidPlatformIconEngine::Glyphs QAndroidPlatformIconEngine::glyphs() const +{ + if (!QFontInfo(m_iconFont).exactMatch()) + return {}; + + static constexpr std::pair glyphMap[] = { + {u"edit-clear", 0xe872}, + {u"edit-copy", 0xe14d}, + {u"edit-cut", 0xe14e}, + {u"edit-delete", 0xe14a}, + {u"edit-find", 0xe8b6}, + {u"edit-find-replace", 0xe881}, + {u"edit-paste", 0xe14f}, + {u"edit-redo", 0xe15a}, + {u"edit-select-all", 0xe162}, + {u"edit-undo", 0xe166}, + {u"printer", 0xe8ad}, + }; + + const auto it = std::find_if(std::begin(glyphMap), std::end(glyphMap), [this](const auto &c){ + return c.first == m_iconName; + }); + return it != std::end(glyphMap) ? it->second : Glyphs(); +} + +QAndroidPlatformIconEngine::QAndroidPlatformIconEngine(const QString &iconName) + : m_iconName(iconName) + , m_glyphs(glyphs()) +{ + // The MaterialIcons-Regular.ttf font file is available from + // https://github.com/google/material-design-icons/tree/master/font. If it's packaged + // as a resource with the application, then we use it. Otherwise we try to download + // the Outlined version of Material Symbols, and failing that we try Material Icons. + QString fontFamily = FontProvider::fetchFont(u":/qt-project.org/icons/MaterialIcons-Regular.ttf"_s); + + const QString key = qEnvironmentVariable("QT_GOOGLE_FONTS_KEY"); + if (fontFamily.isEmpty() && !key.isEmpty()) + fontFamily = FontProvider::fetchFont(u"key=%1&name=Material+Symbols+Outlined"_s.arg(key)); + + // last resort - use the old Material Icons + if (fontFamily.isEmpty()) + fontFamily = u"Material Icons"_s; + m_iconFont = QFont(fontFamily); +} + +QAndroidPlatformIconEngine::~QAndroidPlatformIconEngine() +{} + +QIconEngine *QAndroidPlatformIconEngine::clone() const +{ + return new QAndroidPlatformIconEngine(m_iconName); +} + +QString QAndroidPlatformIconEngine::key() const +{ + return u"QAndroidPlatformIconEngine"_s; +} + +QString QAndroidPlatformIconEngine::iconName() +{ + return m_iconName; +} + +bool QAndroidPlatformIconEngine::isNull() +{ + return m_glyphs.isNull() || !QFontMetrics(m_iconFont).inFont(m_glyphs.codepoints[0]); +} + +QList QAndroidPlatformIconEngine::availableSizes(QIcon::Mode, QIcon::State) +{ + return {{16, 16}, {24, 24}, {48, 48}, {128, 128}}; +} + +QSize QAndroidPlatformIconEngine::actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return QIconEngine::actualSize(size, mode, state); +} + +QPixmap QAndroidPlatformIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return scaledPixmap(size, mode, state, 1.0); +} + +QPixmap QAndroidPlatformIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) +{ + const quint64 cacheKey = calculateCacheKey(mode, state); + if (cacheKey != m_cacheKey || m_pixmap.size() != size || m_pixmap.devicePixelRatio() != scale) { + m_pixmap = QPixmap(size * scale); + m_pixmap.fill(QColor(0, 0, 0, 0)); + m_pixmap.setDevicePixelRatio(scale); + + QPainter painter(&m_pixmap); + QFont renderFont(m_iconFont); + renderFont.setPixelSize(size.height()); + painter.setFont(renderFont); + + QPalette palette; + switch (mode) { + case QIcon::Active: + painter.setPen(palette.color(QPalette::Active, QPalette::Accent)); + break; + case QIcon::Normal: + painter.setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Disabled: + painter.setPen(palette.color(QPalette::Disabled, QPalette::Accent)); + break; + case QIcon::Selected: + painter.setPen(palette.color(QPalette::Active, QPalette::Accent)); + break; + } + + const QRect rect({0, 0}, size); + if (m_glyphs.codepoints[0] == QChar(0xffff)) { + painter.drawText(rect, Qt::AlignCenter, QString(m_glyphs.codepoints + 1, 2)); + } else { + for (const auto &glyph : m_glyphs.codepoints) { + if (glyph.isNull()) + break; + painter.drawText(rect, glyph); + } + } + + m_cacheKey = cacheKey; + } + + return m_pixmap; +} + +void QAndroidPlatformIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) +{ + const qreal scale = painter->device()->devicePixelRatio(); + painter->drawPixmap(rect, scaledPixmap(rect.size(), mode, state, scale)); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformiconengine.h b/src/plugins/platforms/android/qandroidplatformiconengine.h new file mode 100644 index 00000000000..275c01a2947 --- /dev/null +++ b/src/plugins/platforms/android/qandroidplatformiconengine.h @@ -0,0 +1,52 @@ +// Copyright (C) 2023 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 + +#ifndef QANDROIDPLATFORMICONENGINE_H +#define QANDROIDPLATFORMICONENGINE_H + +#include +#include + +QT_BEGIN_NAMESPACE + +class QAndroidPlatformIconEngine : public QIconEngine +{ +public: + QAndroidPlatformIconEngine(const QString &iconName); + ~QAndroidPlatformIconEngine(); + QIconEngine *clone() const override; + QString key() const override; + QString iconName() override; + bool isNull() override; + + QList availableSizes(QIcon::Mode, QIcon::State) override; + QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) override; + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + +private: + static constexpr quint64 calculateCacheKey(QIcon::Mode mode, QIcon::State state) + { + return (quint64(mode) << 32) | state; + } + struct Glyphs + { + constexpr Glyphs(char16_t g1 = 0, char16_t g2 = 0, char16_t g3 = 0) noexcept + : codepoints{g1, g2, g3} + {} + constexpr bool isNull() const noexcept { return codepoints[0].isNull(); } + const QChar codepoints[3] = {}; + }; + Glyphs glyphs() const; + + const QString m_iconName; + QFont m_iconFont; + const Glyphs m_glyphs; + mutable QPixmap m_pixmap; + mutable quint64 m_cacheKey = {}; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDPLATFORMICONENGINE_H diff --git a/src/plugins/platforms/android/qandroidplatformtheme.cpp b/src/plugins/platforms/android/qandroidplatformtheme.cpp index 016bfddc210..d62c1758b88 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.cpp +++ b/src/plugins/platforms/android/qandroidplatformtheme.cpp @@ -4,6 +4,7 @@ #include "androidjnimain.h" #include "androidjnimenu.h" #include "qandroidplatformtheme.h" +#include "qandroidplatformiconengine.h" #include "qandroidplatformmenubar.h" #include "qandroidplatformmenu.h" #include "qandroidplatformmenuitem.h" @@ -481,6 +482,15 @@ const QFont *QAndroidPlatformTheme::font(Font type) const return 0; } +QIconEngine *QAndroidPlatformTheme::createIconEngine(const QString &iconName) const +{ + static bool experimentalIconEngines = qEnvironmentVariableIsSet("QT_ENABLE_EXPERIMENTAL_ICON_ENGINES"); + if (experimentalIconEngines) + return new QAndroidPlatformIconEngine(iconName); + return nullptr; + +} + QVariant QAndroidPlatformTheme::themeHint(ThemeHint hint) const { switch (hint) { diff --git a/src/plugins/platforms/android/qandroidplatformtheme.h b/src/plugins/platforms/android/qandroidplatformtheme.h index bb8c5c48693..d494bad05fa 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.h +++ b/src/plugins/platforms/android/qandroidplatformtheme.h @@ -40,6 +40,7 @@ public: Qt::ColorScheme colorScheme() const override; const QPalette *palette(Palette type = SystemPalette) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; QString standardButtonText(int button) const override; bool usePlatformNativeDialog(DialogType type) const override; diff --git a/tests/manual/iconbrowser/CMakeLists.txt b/tests/manual/iconbrowser/CMakeLists.txt index 4bd22f7eff2..eb304d25a23 100644 --- a/tests/manual/iconbrowser/CMakeLists.txt +++ b/tests/manual/iconbrowser/CMakeLists.txt @@ -13,3 +13,38 @@ qt_internal_add_manual_test(iconbrowser Qt::Widgets Qt::WidgetsPrivate ) + +if (ANDROID) + set(font_filename "MaterialIcons-Regular.ttf") + if (QT_ALLOW_DOWNLOAD) + include(FetchContent) + + FetchContent_Declare( + MaterialIcons + URL + "https://github.com/google/material-design-icons/raw/master/font/${font_filename}" + DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR} + DOWNLOAD_NAME "${font_filename}" + DOWNLOAD_NO_EXTRACT TRUE + ) + + FetchContent_MakeAvailable(MaterialIcons) + endif() + + if (EXISTS "${CMAKE_CURRENT_BINARY_DIR}/${font_filename}") + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/${font_filename}" + PROPERTIES QT_RESOURCE_ALIAS ${font_filename}) + target_compile_definitions(iconbrowser PRIVATE "ICONBROWSER_RESOURCE") + qt_add_resources(iconbrowser "icons" + PREFIX + "/qt-project.org/icons" + FILES + "${CMAKE_CURRENT_BINARY_DIR}/${font_filename}" + ) + else() + message(WARNING "Font file ${font_filename} not found and not downloaded!\n" + "Make sure the font file ${font_filename} is available in ${CMAKE_CURRENT_BINARY_DIR}.\n" + "Consider configuring with -DQT_ALLOW_DOWNLOAD=ON to download the font automatically.") + endif() +endif() + diff --git a/tests/manual/iconbrowser/main.cpp b/tests/manual/iconbrowser/main.cpp index f145485329d..8acff69fa21 100644 --- a/tests/manual/iconbrowser/main.cpp +++ b/tests/manual/iconbrowser/main.cpp @@ -534,6 +534,10 @@ int main(int argc, char* argv[]) QApplication app(argc, argv); +#ifdef ICONBROWSER_RESOURCE + Q_INIT_RESOURCE(icons); +#endif + IconModel model; QTabWidget widget;