Add initial implementation of an Android icon engine

Try to use the Downloadable Font APIs from AndroidX to download the
Material Symbols font. This would ideally allow us to add the official
icon variations dynamically to the device's font cache.

This works for several fonts from Google Fonts, but not for the fonts
we need. So, for the time being, add a path where we consult the
resource system for an embedded font file as well. Then an application
can add e.g. the font file for the desired icons variation, and Qt will
use those glyphs to render icons. Do this in the manual test, using
cmake's FetchContent feature to download the font from Googlei's github
repository.

The incomplete mapping is based on the standard Material icons
documentation at https://fonts.google.com/icons. We could in theory use
the `codepoints` file that comes with the font files to create the
mapping, but then we'd end up with platform specific icon names.

Task-number: QTBUG-102346
Change-Id: Ibff3fe6d310a388e6111d983815ef0ddffb684c8
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
Reviewed-by: Assam Boudjelthia <assam.boudjelthia@qt.io>
This commit is contained in:
Volker Hilsheimer 2023-07-19 10:00:14 +02:00
parent dabf8a0d89
commit f54393ba70
7 changed files with 460 additions and 0 deletions

View File

@ -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

View File

@ -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 <QtCore/qdebug.h>
#include <QtCore/qjniarray.h>
#include <QtCore/qjniobject.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qfile.h>
#include <QtCore/qset.h>
#include <QtGui/qfontdatabase.h>
#include <QtGui/qpainter.h>
#include <QtGui/qpalette.h>
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<QString, QString> 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<PackageManager>("getPackageManager");
if (!packageManager.isValid()) {
qCWarning(lcIconEngineFontDownload, "Failed to instantiate PackageManager");
return fontFamily;
}
const int signaturesField = PackageManager::getStaticField<int>("GET_SIGNATURES");
auto providerInfo = packageManager.callMethod<ProviderInfo>("resolveContentProvider",
authority, 0);
if (!providerInfo.isValid()) {
qCWarning(lcIconEngineFontDownload, "Failed to resolve content provider");
return fontFamily;
}
const QString packageName = providerInfo.getField<QString>("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<PackageInfo>("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<Signature[]>("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<jbyte> byteArray = signature.callMethod<jbyte[]>("toByteArray");
// add takes an Object, not an Array
if (!innerSet.callMethod<jboolean>("add", byteArray.object<jobject>()))
qCWarning(lcIconEngineFontDownload, "Failed to add signature to set");
}
// Add the set to the list
if (!outerList.callMethod<jboolean>("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<List>());
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<FontFamilyResult>(
"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<StatusCode>("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<FontInfo[]>("getFonts");
if (!fontInfos.isValid()) {
qCWarning(lcIconEngineFontDownload, "FontFamilyResult::getFonts returned null object for '%s'",
query.toUtf8().constData());
return fontFamily;
}
auto contentResolver = context.callMethod<ContentResolver>("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<ResultCode>("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<Uri>("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<ParcelFileDescriptor>("openFileDescriptor",
fontUri, u"r"_s);
if (!fileDescriptor.isValid()) {
qCWarning(lcIconEngineFontDownload, "Font file '%s' not accessible",
fontUri.toString().toUtf8().constData());
continue;
}
int fd = fileDescriptor.callMethod<int>("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<QStringView, Glyphs> 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<QSize> 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

View File

@ -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 <QtGui/qiconengine.h>
#include <QtGui/qfont.h>
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<QSize> 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

View File

@ -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) {

View File

@ -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;

View File

@ -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()

View File

@ -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;