Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 120 additions & 9 deletions src/watchfacereloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,144 @@

#include <QQmlEngine>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFontDatabase>
#include <QStandardPaths>

namespace {
constexpr int kSettleIntervalMs = 800;
constexpr int kMaxSettleTries = 6;
constexpr int kMaxLoadAttempts = 8;
}

WatchfaceReloader::WatchfaceReloader(QQmlEngine *engine, QObject *parent)
: QObject(parent)
, m_engine(engine)
{
// Same location the settings watchface store installs into.
m_dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
// Same locations the settings watchface store installs into.
m_watchfaceDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
+ QStringLiteral("/asteroid-launcher/watchfaces");
QDir().mkpath(m_dir);
m_watcher.addPath(m_dir);
m_fontsDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
+ QStringLiteral("/.fonts");

QDir().mkpath(m_watchfaceDir);
QDir().mkpath(m_fontsDir);
m_watcher.addPath(m_watchfaceDir);
m_watcher.addPath(m_fontsDir);

// Register everything already in the user font folder. These fonts exist
// nowhere else, and fontconfig only knows them after an fc-cache run that
// nothing guarantees has happened — on a freshly flashed device it never
// has — so store faces would render with fallback fonts after any session
// restart. Registering an already-known family is harmless.
scanFonts();

connect(&m_watcher, &QFileSystemWatcher::directoryChanged,
this, &WatchfaceReloader::onDirectoryChanged);

// A font pushed by an external writer (adb, scp) may still be mid-write
// when the folder-changed signal fires, so addApplicationFont reads a
// truncated file. Re-scan a short moment later, re-arming while a font on
// disk remains unloaded, so it is registered and applied live once its
// write finishes. (Store installs write to the final path in one shot, so
// in practice this chain serves the external writers.)
m_settleTimer.setSingleShot(true);
m_settleTimer.setInterval(kSettleIntervalMs);
connect(&m_settleTimer, &QTimer::timeout, this, [this]() {
if (scanFonts() > 0)
clearCacheAndReload();
if (m_pendingFonts > 0 && ++m_settleTries < kMaxSettleTries)
m_settleTimer.start();
else
m_settleTries = 0;
});
}

void WatchfaceReloader::onDirectoryChanged(const QString &)
int WatchfaceReloader::scanFonts()
{
static const QStringList kFontFilters{
QStringLiteral("*.ttf"), QStringLiteral("*.otf"), QStringLiteral("*.ttc")};

int added = 0;
m_pendingFonts = 0;
const QFileInfoList files =
QDir(m_fontsDir).entryInfoList(kFontFilters, QDir::Files, QDir::NoSort);
for (const QFileInfo &fi : files) {
const QString path = fi.absoluteFilePath();
const auto it = m_fonts.constFind(path);
if (it != m_fonts.constEnd()) {
if (it->size == fi.size() && it->mtime == fi.lastModified())
continue;
// Replaced in place (store re-download, script push): the database
// still serves the old file's glyphs under this family. Drop and
// re-register below.
QFontDatabase::removeApplicationFont(it->id);
m_fonts.remove(path);
m_attempts.remove(path);
}
if (m_attempts.value(path, 0) >= kMaxLoadAttempts)
continue;
const int id = QFontDatabase::addApplicationFont(path);
if (id != -1) {
m_fonts.insert(path, { id, fi.size(), fi.lastModified() });
m_attempts.remove(path);
++added;
} else {
// Truncated mid-write or genuinely broken; the settle chain
// retries until the attempt budget rules it broken.
++m_attempts[path];
++m_pendingFonts;
}
}

// Files that vanished release their database entry, so an uninstalled
// face's font does not linger for the life of the process.
for (auto rec = m_fonts.begin(); rec != m_fonts.end();) {
if (!QFile::exists(rec.key())) {
QFontDatabase::removeApplicationFont(rec.value().id);
rec = m_fonts.erase(rec);
} else {
++rec;
}
}
return added;
}

void WatchfaceReloader::clearCacheAndReload()
{
if (!m_engine)
return;

// Clearing the component cache forces the next QML load to re-read the
// directory, so a just-installed watchface becomes loadable immediately.
m_engine->clearComponentCache();
emit reloadNeeded();
}

void WatchfaceReloader::onDirectoryChanged(const QString &path)
{
// Route by folder: dropping the component cache tears down and reloads the
// active watchface, so unrelated font-folder churn must not trigger it,
// and a watchface install must not pay a font scan.
if (path == m_fontsDir) {
if (scanFonts() > 0)
clearCacheAndReload();
if (m_pendingFonts > 0) {
m_settleTries = 0;
m_settleTimer.start();
}
} else {
clearCacheAndReload();
}

// QFileSystemWatcher drops a path that momentarily disappears (some tools
// replace directories atomically); re-add it so we keep watching.
if (!m_watcher.directories().contains(m_dir) && QDir(m_dir).exists())
m_watcher.addPath(m_dir);
// replace directories atomically); re-create and re-add so we keep
// watching.
const QStringList watched = m_watcher.directories();
for (const QString &dir : {m_watchfaceDir, m_fontsDir}) {
if (!watched.contains(dir)) {
QDir().mkpath(dir);
m_watcher.addPath(dir);
}
}
}
61 changes: 51 additions & 10 deletions src/watchfacereloader.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,39 @@
#ifndef WATCHFACERELOADER_H
#define WATCHFACERELOADER_H

#include <QObject>
#include <QDateTime>
#include <QFileSystemWatcher>
#include <QHash>
#include <QObject>
#include <QString>
#include <QTimer>

class QQmlEngine;

/*!
* \brief Drops the QML component cache when a watchface is added at runtime.
* \brief Reloads watchfaces and their bundled fonts added at runtime.
*
* Two Qt caches keep a just-installed watchface from showing without a restart:
*
* 1. QQmlTypeLoader reads a directory's listing the first time it resolves a
* file there and then reuses that cached listing for the life of the process,
* with no invalidation. A watchface downloaded into the user watchface folder
* after that first read is invisible to the loader ("File name case mismatch")
* until the component cache is dropped.
* 2. QFontDatabase is populated from fontconfig once at startup. A font a
* watchface bundles into the user font folder afterwards is unknown to the
* running process, so the watchface renders with a fallback family until the
* session is restarted.
*
* Qt's QQmlTypeLoader reads a directory's listing the first time it resolves a
* file there and then reuses that cached listing for the life of the process,
* with no invalidation. A watchface downloaded into the user watchface folder
* after that first read is therefore invisible to the loader ("File name case
* mismatch") until the cache is dropped. This watches the folder and clears the
* component cache when it changes so newly installed watchfaces load live,
* without restarting the launcher.
* This watches both folders and routes each change by its path: a watchface
* change drops the QML component cache; a font change registers new or replaced
* fonts straight into the running font database with
* QFontDatabase::addApplicationFont() (no fc-cache, no restart) and reloads only
* when a font actually arrived. The user fonts are also registered at startup:
* they exist only in the user folder, and fontconfig knows them only after an
* fc-cache run nothing guarantees has happened (a fresh install never ran one),
* so relying on fontconfig would leave every store face on fallback fonts after
* a session restart.
*/
class WatchfaceReloader : public QObject
{
Expand All @@ -39,9 +56,33 @@ private slots:
void onDirectoryChanged(const QString &path);

private:
//! One pass over the user font folder: register fonts not yet in the
//! running database, re-register files replaced in place (same path, new
//! size or mtime), and drop database entries for files that vanished.
//! Returns how many fonts newly registered; sets m_pendingFonts to the
//! number present on disk but not (yet) loadable.
int scanFonts();

void clearCacheAndReload();

struct FontRecord {
int id = -1;
qint64 size = 0;
QDateTime mtime;
};

QQmlEngine *m_engine;
QFileSystemWatcher m_watcher;
QString m_dir;
QTimer m_settleTimer;
int m_settleTries = 0;
int m_pendingFonts = 0;
QString m_watchfaceDir;
QString m_fontsDir;
QHash<QString, FontRecord> m_fonts;
//! Load attempts per path; a file that keeps failing (corrupt or an
//! unsupported format) stops being retried once its budget is spent, so it
//! cannot re-arm the settle chain on every later folder change.
QHash<QString, int> m_attempts;
};

#endif // WATCHFACERELOADER_H