summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/app.pro8
-rw-r--r--app/pebble.cpp12
-rw-r--r--app/pebbleappiconprovider.cpp28
-rw-r--r--app/pebbleappiconprovider.h18
-rw-r--r--app/pebbledinterface.cpp18
-rw-r--r--app/pebbledinterface.h6
-rw-r--r--app/qml/pages/AppConfigPage.qml14
-rw-r--r--app/qml/pages/InstallAppDialog.qml17
-rw-r--r--app/qml/pages/WatchPage.qml49
-rw-r--r--app/qml/pebble.qml4
-rw-r--r--daemon/appinfo.cpp25
-rw-r--r--daemon/appinfo.h9
-rw-r--r--daemon/appmanager.cpp142
-rw-r--r--daemon/appmanager.h2
-rw-r--r--daemon/manager.cpp5
15 files changed, 315 insertions, 42 deletions
diff --git a/app/app.pro b/app/app.pro
index d375800..48fcf68 100644
--- a/app/app.pro
+++ b/app/app.pro
@@ -3,16 +3,18 @@ TARGET = pebble
CONFIG += sailfishapp
QT += dbus
-QMAKE_CXXFLAGS += -std=c++0x
+CONFIG += c++11
DEFINES += APP_VERSION=\\\"$$VERSION\\\"
SOURCES += \
pebble.cpp \
- pebbledinterface.cpp
+ pebbledinterface.cpp \
+ pebbleappiconprovider.cpp
HEADERS += \
- pebbledinterface.h
+ pebbledinterface.h \
+ pebbleappiconprovider.h
DBUS_INTERFACES += ../org.pebbled.Watch.xml
diff --git a/app/pebble.cpp b/app/pebble.cpp
index 44f1aeb..41da080 100644
--- a/app/pebble.cpp
+++ b/app/pebble.cpp
@@ -33,16 +33,22 @@
#include <sailfishapp.h>
#include "pebbledinterface.h"
+#include "pebbleappiconprovider.h"
int main(int argc, char *argv[])
{
- // Register Pebble daemon interface object on QML side
- qmlRegisterType<PebbledInterface>("org.pebbled", 0, 1, "PebbledInterface");
-
QScopedPointer<QGuiApplication> app(SailfishApp::application(argc, argv));
+ qmlRegisterUncreatableType<PebbledInterface>("org.pebbled", 0, 1, "PebbledInterface",
+ "Please use pebbled context property");
+
QScopedPointer<QQuickView> view(SailfishApp::createView());
+ QScopedPointer<PebbledInterface> pebbled(new PebbledInterface);
+ QScopedPointer<PebbleAppIconProvider> appicons(new PebbleAppIconProvider(pebbled.data()));
+
view->rootContext()->setContextProperty("APP_VERSION", APP_VERSION);
+ view->rootContext()->setContextProperty("pebbled", pebbled.data());
+ view->engine()->addImageProvider("pebble-app-icon", appicons.data());
view->setSource(SailfishApp::pathTo("qml/pebble.qml"));
view->show();
diff --git a/app/pebbleappiconprovider.cpp b/app/pebbleappiconprovider.cpp
new file mode 100644
index 0000000..0e694ff
--- /dev/null
+++ b/app/pebbleappiconprovider.cpp
@@ -0,0 +1,28 @@
+#include <QDebug>
+#include <QUrl>
+#include "pebbleappiconprovider.h"
+
+PebbleAppIconProvider::PebbleAppIconProvider(PebbledInterface *interface)
+ : QQuickImageProvider(QQmlImageProviderBase::Image), pebbled(interface)
+{
+}
+
+QImage PebbleAppIconProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
+{
+ QUuid uuid(QUrl::fromPercentEncoding(id.toLatin1()));
+ QImage img = pebbled->menuIconForApp(uuid);
+
+ if (requestedSize.width() > 0 && requestedSize.height() > 0) {
+ img = img.scaled(requestedSize, Qt::KeepAspectRatio);
+ } else if (requestedSize.width() > 0) {
+ img = img.scaledToWidth(requestedSize.width());
+ } else if (requestedSize.height() > 0) {
+ img = img.scaledToHeight(requestedSize.height());
+ }
+
+ if (size) {
+ *size = img.size();
+ }
+
+ return img;
+}
diff --git a/app/pebbleappiconprovider.h b/app/pebbleappiconprovider.h
new file mode 100644
index 0000000..c76641a
--- /dev/null
+++ b/app/pebbleappiconprovider.h
@@ -0,0 +1,18 @@
+#ifndef PEBBLEAPPICONPROVIDER_H
+#define PEBBLEAPPICONPROVIDER_H
+
+#include <QQuickImageProvider>
+#include "pebbledinterface.h"
+
+class PebbleAppIconProvider : public QQuickImageProvider
+{
+public:
+ explicit PebbleAppIconProvider(PebbledInterface *interface);
+
+ QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize);
+
+private:
+ PebbledInterface *pebbled;
+};
+
+#endif // PEBBLEAPPICONPROVIDER_H
diff --git a/app/pebbledinterface.cpp b/app/pebbledinterface.cpp
index 588e24a..c978dd0 100644
--- a/app/pebbledinterface.cpp
+++ b/app/pebbledinterface.cpp
@@ -183,7 +183,7 @@ QUrl PebbledInterface::configureApp(const QString &uuid)
}
}
-bool PebbledInterface::isAppInstalled(const QString &uuid)
+bool PebbledInterface::isAppInstalled(const QString &uuid) const
{
QUuid u(uuid);
@@ -196,6 +196,11 @@ bool PebbledInterface::isAppInstalled(const QString &uuid)
return false;
}
+QImage PebbledInterface::menuIconForApp(const QUuid &uuid) const
+{
+ return _appMenuIcons.value(uuid);
+}
+
void PebbledInterface::setAppConfiguration(const QString &uuid, const QString &data)
{
qDebug() << Q_FUNC_INFO << uuid << data;
@@ -210,8 +215,11 @@ void PebbledInterface::launchApp(const QString &uuid)
// TODO Terrible hack; need to give time for the watch to open the app
// A better solution would be to wait until AppUuidChanged is generated.
+ QUuid u(uuid);
+ if (u.isNull()) return;
int sleep_count = 0;
- while (watch->appUuid() != uuid && sleep_count < 5) {
+ while (QUuid(watch->appUuid()) != u && sleep_count < 5) {
+ qDebug() << "Waiting for" << u.toString() << "to launch";
QThread::sleep(1);
sleep_count++;
}
@@ -271,6 +279,7 @@ void PebbledInterface::refreshAllApps()
{
_apps.clear();
_appsByUuid.clear();
+ _appMenuIcons.clear();
qDebug() << "refreshing all apps list";
@@ -288,6 +297,11 @@ void PebbledInterface::refreshAllApps()
m.insert("shortName", orig.value("short-name"));
m.insert("longName", orig.value("long-name"));
+ QByteArray pngIcon = orig.value("menu-icon").toByteArray();
+ if (!pngIcon.isEmpty()) {
+ _appMenuIcons.insert(uuid, QImage::fromData(pngIcon, "PNG"));
+ }
+
_apps.append(QVariant::fromValue(m));
}
diff --git a/app/pebbledinterface.h b/app/pebbledinterface.h
index e468505..51efa12 100644
--- a/app/pebbledinterface.h
+++ b/app/pebbledinterface.h
@@ -5,6 +5,7 @@
#include <QUrl>
#include <QHash>
#include <QUuid>
+#include <QImage>
#include <QDBusInterface>
class OrgPebbledWatchInterface;
@@ -39,7 +40,9 @@ public:
Q_INVOKABLE QUrl configureApp(const QString &uuid);
- Q_INVOKABLE bool isAppInstalled(const QString &uuid);
+ Q_INVOKABLE bool isAppInstalled(const QString &uuid) const;
+
+ QImage menuIconForApp(const QUuid &uuid) const;
signals:
void enabledChanged();
@@ -82,6 +85,7 @@ private:
QStringList _appSlots;
QVariantList _apps;
QHash<QUuid, int> _appsByUuid;
+ QHash<QUuid, QImage> _appMenuIcons;
};
#endif // PEBBLEDINTERFACE_H
diff --git a/app/qml/pages/AppConfigPage.qml b/app/qml/pages/AppConfigPage.qml
index 10fbe05..00eb05c 100644
--- a/app/qml/pages/AppConfigPage.qml
+++ b/app/qml/pages/AppConfigPage.qml
@@ -11,6 +11,7 @@ Page {
SilicaWebView {
id: webview
+ visible: url != ""
anchors.fill: parent
header: PageHeader {
@@ -32,8 +33,17 @@ Page {
}
}
- ViewPlaceholder {
- enabled: url == ""
+ Text {
+ anchors.centerIn: parent
+ visible: url == ""
text: qsTr("No configuration settings available")
+ width: parent.width - 2*Theme.paddingLarge
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.Wrap
+ font {
+ pixelSize: Theme.fontSizeLarge
+ family: Theme.fontFamilyHeading
+ }
+ color: Theme.highlightColor
}
}
diff --git a/app/qml/pages/InstallAppDialog.qml b/app/qml/pages/InstallAppDialog.qml
index 3a3c0b1..79283a6 100644
--- a/app/qml/pages/InstallAppDialog.qml
+++ b/app/qml/pages/InstallAppDialog.qml
@@ -27,20 +27,29 @@ Dialog {
property string uuid: modelData.uuid
property bool alreadyInstalled: pebbled.isAppInstalled(uuid)
- Image {
- id: appImage
+ Item {
+ id: appIcon
+ width: Theme.itemSizeSmall
+ height: Theme.itemSizeSmall
+
anchors {
top: parent.top
left: parent.left
leftMargin: Theme.paddingLarge
}
- width: Theme.itemSizeSmall
+
+ Image {
+ id: appImage
+ anchors.centerIn: parent
+ source: "image://pebble-app-icon/" + uuid;
+ scale: 2
+ }
}
Label {
id: appName
anchors {
- left: appImage.right
+ left: appIcon.right
leftMargin: Theme.paddingMedium
right: parent.right
rightMargin: Theme.paddiumLarge
diff --git a/app/qml/pages/WatchPage.qml b/app/qml/pages/WatchPage.qml
index ce9d636..3a712ab 100644
--- a/app/qml/pages/WatchPage.qml
+++ b/app/qml/pages/WatchPage.qml
@@ -77,7 +77,8 @@ Page {
}
Item {
- height: Theme.paddingMedium
+ width: parent.width
+ height: Theme.paddingLarge
}
Label {
@@ -139,26 +140,49 @@ Page {
}
- Image {
- id: slotImage
+ Item {
+ id: slotIcon
+ width: Theme.itemSizeSmall
+ height: Theme.itemSizeSmall
+
anchors {
top: parent.top
left: parent.left
leftMargin: Theme.paddingLarge
}
- width: Theme.itemSizeSmall
- }
- BusyIndicator {
- id: slotBusy
- anchors.centerIn: slotImage
- running: slotDelegate.busy
+ Image {
+ id: slotImage
+ anchors.centerIn: parent
+ source: isKnownApp ? "image://pebble-app-icon/" + modelData : ""
+ scale: 2
+ visible: !isEmptySlot && isKnownApp && !slotBusy.running
+ }
+
+ Rectangle {
+ width: 30
+ height: 30
+ anchors.centerIn: parent
+ scale: 2
+ border {
+ width: 2
+ color: slotDelegate.highlighted ? Theme.highlightColor : Theme.primaryColor
+ }
+ color: "transparent"
+ visible: isEmptySlot && !slotBusy.running
+ }
+
+ BusyIndicator {
+ id: slotBusy
+ anchors.centerIn: parent
+ running: slotDelegate.busy
+ }
}
Label {
id: slotName
anchors {
- left: slotImage.right
+ left: slotIcon.right
leftMargin: Theme.paddingMedium
right: parent.right
rightMargin: Theme.paddiumLarge
@@ -173,6 +197,11 @@ Page {
id: slotMenu
ContextMenu {
MenuItem {
+ text: qsTr("Install app...")
+ visible: isEmptySlot
+ onClicked: install();
+ }
+ MenuItem {
text: qsTr("Configure...")
visible: !isEmptySlot && isKnownApp
onClicked: configure();
diff --git a/app/qml/pebble.qml b/app/qml/pebble.qml
index da3bfb5..2e26ebe 100644
--- a/app/qml/pebble.qml
+++ b/app/qml/pebble.qml
@@ -38,8 +38,4 @@ ApplicationWindow
{
initialPage: Component { ManagerPage { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
-
- PebbledInterface {
- id: pebbled
- }
}
diff --git a/daemon/appinfo.cpp b/daemon/appinfo.cpp
index fd43248..4397abc 100644
--- a/daemon/appinfo.cpp
+++ b/daemon/appinfo.cpp
@@ -1,5 +1,6 @@
-#include "appinfo.h"
#include <QSharedData>
+#include <QBuffer>
+#include "appinfo.h"
struct AppInfoData : public QSharedData {
QUuid uuid;
@@ -13,6 +14,7 @@ struct AppInfoData : public QSharedData {
AppInfo::Capabilities capabilities;
QHash<QString, int> keyInts;
QHash<int, QString> keyNames;
+ QImage menuIcon;
QString path;
};
@@ -21,6 +23,7 @@ AppInfo::AppInfo() : d(new AppInfoData)
d->versionCode = 0;
d->watchface = false;
d->jskit = false;
+ d->capabilities = 0;
}
AppInfo::AppInfo(const AppInfo &rhs) : d(rhs.d)
@@ -154,6 +157,26 @@ int AppInfo::valueForAppKey(const QString &key) const
return d->keyInts.value(key, -1);
}
+QImage AppInfo::menuIcon() const
+{
+ return d->menuIcon;
+}
+
+QByteArray AppInfo::menuIconAsPng() const
+{
+ QByteArray data;
+ QBuffer buf(&data);
+ buf.open(QIODevice::WriteOnly);
+ d->menuIcon.save(&buf, "PNG");
+ buf.close();
+ return data;
+}
+
+void AppInfo::setMenuIcon(const QImage &img)
+{
+ d->menuIcon = img;
+}
+
QString AppInfo::path() const
{
return d->path;
diff --git a/daemon/appinfo.h b/daemon/appinfo.h
index 6f97639..3d5c4b4 100644
--- a/daemon/appinfo.h
+++ b/daemon/appinfo.h
@@ -1,10 +1,10 @@
#ifndef APPINFO_H
#define APPINFO_H
-#include <QObject>
+#include <QSharedDataPointer>
#include <QUuid>
#include <QHash>
-#include <QSharedDataPointer>
+#include <QImage>
class AppInfoData;
@@ -28,6 +28,7 @@ public:
Q_PROPERTY(bool watchface READ isWatchface WRITE setWatchface)
Q_PROPERTY(bool jskit READ isJSKit WRITE setJSKit)
Q_PROPERTY(Capabilities capabilities READ capabilities WRITE setCapabilities)
+ Q_PROPERTY(QImage menuIcon READ menuIcon WRITE setMenuIcon)
Q_PROPERTY(QString path READ path WRITE setPath)
public:
@@ -71,6 +72,10 @@ public:
bool hasAppKey(const QString &key) const;
int valueForAppKey(const QString &key) const;
+ QImage menuIcon() const;
+ QByteArray menuIconAsPng() const;
+ void setMenuIcon(const QImage &img);
+
QString path() const;
void setPath(const QString &string);
diff --git a/daemon/appmanager.cpp b/daemon/appmanager.cpp
index 10f2e3e..8745160 100644
--- a/daemon/appmanager.cpp
+++ b/daemon/appmanager.cpp
@@ -4,6 +4,17 @@
#include <QJsonArray>
#include <QDir>
#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),
@@ -116,18 +127,55 @@ void AppManager::scanApp(const QString &path)
info.setWatchface(watchapp["watchface"].toBool());
info.setJSKit(appDir.exists("pebble-js-app.js"));
- const QJsonArray capabilities = root["capabilities"].toArray();
- AppInfo::Capabilities caps = 0;
- for (QJsonArray::const_iterator it = capabilities.constBegin(); it != capabilities.constEnd(); ++it) {
- QString cap = (*it).toString();
- if (cap == "location") caps |= AppInfo::Location;
- if (cap == "configurable") caps |= AppInfo::Configurable;
+ 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);
}
- info.setCapabilities(caps);
- const QJsonObject appkeys = root["appKeys"].toObject();
- for (QJsonObject::const_iterator it = appkeys.constBegin(); it != appkeys.constEnd(); ++it) {
- info.addAppKey(it.key(), it.value().toInt());
+ 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);
@@ -143,3 +191,77 @@ void AppManager::scanApp(const QString &path)
const char *type = info.isWatchface() ? "watchface" : "app";
logger()->debug() << "found installed" << type << info.shortName() << info.versionLabel() << "with uuid" << info.uuid().toString();
}
+
+QByteArray AppManager::extractFromResourcePack(const QString &file, int wanted_id) const
+{
+ QFile f(file);
+ if (!f.open(QIODevice::ReadOnly)) {
+ logger()->warn() << "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
+
+ logger()->debug() << "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()) {
+ logger()->warn() << "short read on resource file";
+ return QByteArray();
+ }
+
+ table.append(e);
+ }
+
+ if (wanted_id >= table.size()) {
+ logger()->warn() << "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) {
+ logger()->warn() << "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<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;
+}
diff --git a/daemon/appmanager.h b/daemon/appmanager.h
index 1725c14..d5e5ba1 100644
--- a/daemon/appmanager.h
+++ b/daemon/appmanager.h
@@ -30,6 +30,8 @@ signals:
private:
void scanApp(const QString &path);
+ QByteArray extractFromResourcePack(const QString &file, int id) const;
+ QImage decodeResourceImage(const QByteArray &data) const;
private:
QFileSystemWatcher *_watcher;
diff --git a/daemon/manager.cpp b/daemon/manager.cpp
index 25908e4..212a1d7 100644
--- a/daemon/manager.cpp
+++ b/daemon/manager.cpp
@@ -380,6 +380,11 @@ QVariantList PebbledProxy::AllApps() const
m.insert("company-name", QVariant::fromValue(info.companyName()));
m.insert("version-label", QVariant::fromValue(info.versionLabel()));
m.insert("is-watchface", QVariant::fromValue(info.isWatchface()));
+
+ if (!info.menuIcon().isNull()) {
+ m.insert("menu-icon", QVariant::fromValue(info.menuIconAsPng()));
+ }
+
l.append(QVariant::fromValue(m));
}