#include #include #include #include #include #include "appmanager.h" #include "unpacker.h" #include "stm32crc.h" namespace { struct ResourceEntry { int index; quint32 offset; quint32 length; quint32 crc; }; } AppManager::AppManager(QObject *parent) : QObject(parent), l(metaObject()->className()), _watcher(new QFileSystemWatcher(this)) { connect(_watcher, &QFileSystemWatcher::directoryChanged, this, &AppManager::rescan); QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); if (!dataDir.mkpath("apps")) { qCWarning(l) << "could not create apps dir" << dataDir.absoluteFilePath("apps"); } qCDebug(l) << "install apps in" << dataDir.absoluteFilePath("apps"); rescan(); } QStringList AppManager::appPaths() const { return QStandardPaths::locateAll(QStandardPaths::DataLocation, QLatin1String("apps"), QStandardPaths::LocateDirectory); } QList AppManager::appUuids() const { return _apps.keys(); } AppInfo AppManager::info(const QUuid &uuid) const { return _apps.value(uuid); } AppInfo AppManager::info(const QString &name) const { QUuid uuid = _names.value(name); if (!uuid.isNull()) { return info(uuid); } else { return AppInfo(); } } void AppManager::rescan() { QStringList watchedDirs = _watcher->directories(); if (!watchedDirs.isEmpty()) _watcher->removePaths(watchedDirs); QStringList watchedFiles = _watcher->files(); if (!watchedFiles.isEmpty()) _watcher->removePaths(watchedFiles); _apps.clear(); _names.clear(); Q_FOREACH(const QString &path, appPaths()) { QDir dir(path); _watcher->addPath(dir.absolutePath()); qCDebug(l) << "scanning dir" << dir.absolutePath(); QStringList entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Executable); qCDebug(l) << "scanning dir results" << entries; Q_FOREACH(const QString &path, entries) { QString appPath = dir.absoluteFilePath(path); _watcher->addPath(appPath); if (dir.exists(path + "/appinfo.json")) { _watcher->addPath(appPath + "/appinfo.json"); scanApp(appPath); } } } qCDebug(l) << "now watching" << _watcher->directories() << _watcher->files(); emit appsChanged(); } void AppManager::insertAppInfo(const AppInfo &info) { _apps.insert(info.uuid(), info); _names.insert(info.shortName(), info.uuid()); const char *type = info.isWatchface() ? "watchface" : "app"; const char *local = info.isLocal() ? "local" : "watch"; qCDebug(l) << "found" << local << type << info.shortName() << info.versionCode() << "/" << info.versionLabel() << "with uuid" << info.uuid().toString(); } void AppManager::scanApp(const QString &path) { qCDebug(l) << "scanning app" << path; QDir appDir(path); if (!appDir.isReadable()) { qCWarning(l) << "app" << appDir.absolutePath() << "is not readable"; return; } QFile appInfoFile(path + "/appinfo.json"); if (!appInfoFile.open(QIODevice::ReadOnly | QIODevice::Text)) { qCWarning(l) << "cannot open app info file" << appInfoFile.fileName() << ":" << appInfoFile.errorString(); return; } QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(appInfoFile.readAll(), &parseError); if (parseError.error != QJsonParseError::NoError) { qCWarning(l) << "cannot parse app info file" << appInfoFile.fileName() << ":" << parseError.errorString(); return; } const QJsonObject root = doc.object(); AppInfo info; info.setLocal(true); info.setUuid(QUuid(root["uuid"].toString())); info.setShortName(root["shortName"].toString()); info.setLongName(root["longName"].toString()); info.setCompanyName(root["companyName"].toString()); info.setVersionCode(root["versionCode"].toInt()); info.setVersionLabel(root["versionLabel"].toString()); const QJsonObject watchapp = root["watchapp"].toObject(); info.setWatchface(watchapp["watchface"].toBool()); info.setJSKit(appDir.exists("pebble-js-app.js")); if (root.contains("capabilities")) { const QJsonArray capabilities = root["capabilities"].toArray(); AppInfo::Capabilities caps = 0; for (auto it = capabilities.constBegin(); it != capabilities.constEnd(); ++it) { QString cap = (*it).toString(); if (cap == "location") caps |= AppInfo::Location; if (cap == "configurable") caps |= AppInfo::Configurable; } info.setCapabilities(caps); } if (root.contains("appKeys")) { const QJsonObject appkeys = root["appKeys"].toObject(); for (auto it = appkeys.constBegin(); it != appkeys.constEnd(); ++it) { info.addAppKey(it.key(), it.value().toInt()); } } if (root.contains("resources")) { const QJsonObject resources = root["resources"].toObject(); const QJsonArray media = resources["media"].toArray(); int index = 0; for (auto it = media.constBegin(); it != media.constEnd(); ++it) { const QJsonObject res = (*it).toObject(); const QJsonValue menuIcon = res["menuIcon"]; bool is_menu_icon = false; switch (menuIcon.type()) { case QJsonValue::Bool: is_menu_icon = menuIcon.toBool(); break; case QJsonValue::String: is_menu_icon = !menuIcon.toString().isEmpty(); break; default: break; } if (is_menu_icon) { QByteArray data = extractFromResourcePack(appDir.filePath("app_resources.pbpack"), index); if (!data.isEmpty()) { QImage icon = decodeResourceImage(data); info.setMenuIcon(icon); } } index++; } } info.setPath(path); if (info.uuid().isNull() || info.shortName().isEmpty()) { qCWarning(l) << "invalid or empty uuid/name in" << appInfoFile.fileName(); return; } insertAppInfo(info); } QByteArray AppManager::extractFromResourcePack(const QString &file, int wanted_id) const { QFile f(file); if (!f.open(QIODevice::ReadOnly)) { qCWarning(l) << "cannot open resource file" << f.fileName(); return QByteArray(); } QByteArray data = f.readAll(); Unpacker u(data); int num_files = u.readLE(); u.readLE(); // crc for entire file u.readLE(); // timestamp qCDebug(l) << "reading" << num_files << "resources from" << file; QList table; for (int i = 0; i < num_files; i++) { ResourceEntry e; e.index = u.readLE(); e.offset = u.readLE(); e.length = u.readLE(); e.crc = u.readLE(); if (u.bad()) { qCWarning(l) << "short read on resource file"; return QByteArray(); } table.append(e); } if (wanted_id >= table.size()) { qCWarning(l) << "specified resource does not exist"; return QByteArray(); } const ResourceEntry &e = table[wanted_id]; int offset = 12 + 256 * 16 + e.offset; QByteArray res = data.mid(offset, e.length); Stm32Crc crc; crc.addData(res); if (crc.result() != e.crc) { qCWarning(l) << "CRC failure in resource" << e.index << "on file" << file; return QByteArray(); } return res; } QImage AppManager::decodeResourceImage(const QByteArray &data) const { Unpacker u(data); int scanline = u.readLE(); u.skip(sizeof(quint16) + sizeof(quint32)); int width = u.readLE(); int height = u.readLE(); QImage img(width, height, QImage::Format_MonoLSB); const uchar *src = reinterpret_cast(&data.constData()[12]); for (int line = 0; line < height; ++line) { memcpy(img.scanLine(line), src, qMin(scanline, img.bytesPerLine())); src += scanline; } return img; }