summaryrefslogtreecommitdiff
path: root/daemon/appinfo.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'daemon/appinfo.cpp')
-rw-r--r--daemon/appinfo.cpp309
1 files changed, 249 insertions, 60 deletions
diff --git a/daemon/appinfo.cpp b/daemon/appinfo.cpp
index 587ed8b..91467b0 100644
--- a/daemon/appinfo.cpp
+++ b/daemon/appinfo.cpp
@@ -1,9 +1,23 @@
#include <QSharedData>
#include <QBuffer>
+#include <QDir>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
#include "appinfo.h"
+#include "unpacker.h"
+#include "stm32crc.h"
+
+namespace {
+struct ResourceEntry {
+ int index;
+ quint32 offset;
+ quint32 length;
+ quint32 crc;
+};
+}
struct AppInfoData : public QSharedData {
- bool local;
QUuid uuid;
QString shortName;
QString longName;
@@ -15,17 +29,21 @@ struct AppInfoData : public QSharedData {
AppInfo::Capabilities capabilities;
QHash<QString, int> keyInts;
QHash<int, QString> keyNames;
- QImage menuIcon;
+ bool menuIcon;
+ int menuIconResource;
QString path;
};
+QLoggingCategory AppInfo::l("AppInfo");
+
AppInfo::AppInfo() : d(new AppInfoData)
{
- d->local = false;
d->versionCode = 0;
d->watchface = false;
d->jskit = false;
d->capabilities = 0;
+ d->menuIcon = false;
+ d->menuIconResource = -1;
}
AppInfo::AppInfo(const AppInfo &rhs) : d(rhs.d)
@@ -45,22 +63,22 @@ AppInfo::~AppInfo()
bool AppInfo::isLocal() const
{
- return d->local;
+ return ! d->path.isEmpty();
}
-void AppInfo::setLocal(const bool local)
+bool AppInfo::isValid() const
{
- d->local = local;
+ return ! d->uuid.isNull();
}
-QUuid AppInfo::uuid() const
+void AppInfo::setInvalid()
{
- return d->uuid;
+ d->uuid = QUuid(); // Clear the uuid to force invalid app
}
-void AppInfo::setUuid(const QUuid &uuid)
+QUuid AppInfo::uuid() const
{
- d->uuid = uuid;
+ return d->uuid;
}
QString AppInfo::shortName() const
@@ -68,81 +86,41 @@ QString AppInfo::shortName() const
return d->shortName;
}
-void AppInfo::setShortName(const QString &string)
-{
- d->shortName = string;
-}
-
QString AppInfo::longName() const
{
return d->longName;
}
-void AppInfo::setLongName(const QString &string)
-{
- d->longName = string;
-}
-
QString AppInfo::companyName() const
{
return d->companyName;
}
-void AppInfo::setCompanyName(const QString &string)
-{
- d->companyName = string;
-}
-
int AppInfo::versionCode() const
{
return d->versionCode;
}
-void AppInfo::setVersionCode(int code)
-{
- d->versionCode = code;
-}
-
QString AppInfo::versionLabel() const
{
return d->versionLabel;
}
-void AppInfo::setVersionLabel(const QString &string)
-{
- d->versionLabel = string;
-}
-
bool AppInfo::isWatchface() const
{
return d->watchface;
}
-void AppInfo::setWatchface(bool b)
-{
- d->watchface = b;
-}
-
bool AppInfo::isJSKit() const
{
return d->jskit;
}
-void AppInfo::setJSKit(bool b)
-{
- d->jskit = b;
-}
-
AppInfo::Capabilities AppInfo::capabilities() const
{
return d->capabilities;
}
-void AppInfo::setCapabilities(Capabilities caps)
-{
- d->capabilities = caps;
-}
-
void AppInfo::addAppKey(const QString &key, int value)
{
d->keyInts.insert(key, value);
@@ -169,32 +147,243 @@ int AppInfo::valueForAppKey(const QString &key) const
return d->keyInts.value(key, -1);
}
-QImage AppInfo::menuIcon() const
+bool AppInfo::hasMenuIcon() const
+{
+ return d->menuIcon && d->menuIconResource >= 0;
+}
+
+QImage AppInfo::getMenuIconImage() const
{
- return d->menuIcon;
+ if (hasMenuIcon()) {
+ QByteArray data = extractFromResourcePack(
+ QDir(d->path).filePath("app_resources.pbpack"), d->menuIconResource);
+ if (!data.isEmpty()) {
+ return decodeResourceImage(data);
+ }
+ }
+
+ return QImage();
}
-QByteArray AppInfo::menuIconAsPng() const
+QByteArray AppInfo::getMenuIconPng() const
{
QByteArray data;
QBuffer buf(&data);
buf.open(QIODevice::WriteOnly);
- d->menuIcon.save(&buf, "PNG");
+ getMenuIconImage().save(&buf, "PNG");
buf.close();
return data;
}
-void AppInfo::setMenuIcon(const QImage &img)
+QString AppInfo::getJSApp() const
+{
+ if (!isValid() || !isLocal()) return QString();
+
+ QFile file(d->path + "/pebble-js-app.js");
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qCWarning(l) << "Failed to load JS file:" << file.fileName();
+ return QString();
+ }
+
+ return QString::fromUtf8(file.readAll());
+
+}
+
+AppInfo AppInfo::fromPath(const QString &path)
+{
+ AppInfo info;
+
+ QDir appDir(path);
+ if (!appDir.isReadable()) {
+ qCWarning(l) << "app" << appDir.absolutePath() << "is not readable";
+ return info;
+ }
+
+ QFile appInfoFile(path + "/appinfo.json");
+ if (!appInfoFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qCWarning(l) << "cannot open app info file" << appInfoFile.fileName() << ":"
+ << appInfoFile.errorString();
+ return info;
+ }
+
+ 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 info;
+ }
+
+ const QJsonObject root = doc.object();
+ info.d->uuid = QUuid(root["uuid"].toString());
+ info.d->shortName = root["shortName"].toString();
+ info.d->longName = root["longName"].toString();
+ info.d->companyName = root["companyName"].toString();
+ info.d->versionCode = root["versionCode"].toInt();
+ info.d->versionLabel = root["versionLabel"].toString();
+
+ const QJsonObject watchapp = root["watchapp"].toObject();
+ info.d->watchface = watchapp["watchface"].toBool();
+ info.d->jskit = 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.d->capabilities = 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"];
+
+ switch (menuIcon.type()) {
+ case QJsonValue::Bool:
+ info.d->menuIcon = menuIcon.toBool();
+ info.d->menuIconResource = index;
+ break;
+ case QJsonValue::String:
+ info.d->menuIcon = !menuIcon.toString().isEmpty();
+ info.d->menuIconResource = index;
+ break;
+ default:
+ break;
+ }
+
+ index++;
+ }
+ }
+
+ info.d->path = path;
+
+ if (info.uuid().isNull() || info.shortName().isEmpty()) {
+ qCWarning(l) << "invalid or empty uuid/name in" << appInfoFile.fileName();
+ return AppInfo();
+ }
+
+ return info;
+}
+
+AppInfo AppInfo::fromSlot(const BankManager::SlotInfo &slot)
{
- d->menuIcon = img;
+ AppInfo info;
+
+ info.d->uuid = QUuid::createUuid();
+ info.d->shortName = slot.name;
+ info.d->companyName = slot.company;
+ info.d->versionCode = slot.version;
+ info.d->capabilities = AppInfo::Capabilities(slot.flags);
+
+ return info;
+}
+
+QByteArray AppInfo::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<quint32>();
+ u.readLE<quint32>(); // crc for entire file
+ u.readLE<quint32>(); // timestamp
+
+ qCDebug(l) << "reading" << num_files << "resources from" << file;
+
+ QList<ResourceEntry> table;
+
+ for (int i = 0; i < num_files; i++) {
+ ResourceEntry e;
+ e.index = u.readLE<quint32>();
+ e.offset = u.readLE<quint32>();
+ e.length = u.readLE<quint32>();
+ e.crc = u.readLE<quint32>();
+
+ 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;
}
-QString AppInfo::path() const
+QImage AppInfo::decodeResourceImage(const QByteArray &data) const
{
- return d->path;
+ Unpacker u(data);
+ int scanline = u.readLE<quint16>();
+ u.skip(sizeof(quint16) + sizeof(quint32));
+ int width = u.readLE<quint16>();
+ int height = u.readLE<quint16>();
+
+ QImage img(width, height, QImage::Format_MonoLSB);
+ const uchar *src = reinterpret_cast<const uchar *>(&data.constData()[12]);
+ for (int line = 0; line < height; ++line) {
+ memcpy(img.scanLine(line), src, qMin(scanline, img.bytesPerLine()));
+ src += scanline;
+ }
+
+ return img;
}
-void AppInfo::setPath(const QString &string)
+// TODO: abstract to QIOReader
+QString AppInfo::filePath(enum AppInfo::File file) const
{
- d->path = string;
+ QString fileName;
+ switch (file) {
+ case AppInfo::BINARY:
+ fileName = "pebble-app.bin";
+ break;
+ case AppInfo::RESOURCES:
+ fileName = "app_resources.pbpack";
+ break;
+ }
+
+ QDir appDir(d->path);
+ if (appDir.exists(fileName)) {
+ return appDir.absoluteFilePath(fileName);
+ }
+
+ return QString();
}