diff options
| author | Tomasz Sterna <tomek@xiaoka.com> | 2015-01-03 16:38:02 +0100 |
|---|---|---|
| committer | Tomasz Sterna <tomek@xiaoka.com> | 2015-01-03 19:07:48 +0100 |
| commit | 4e7da1944f5fa75a0739c0757d40a8102f045365 (patch) | |
| tree | 5f3fe179256536e4135eb4d5031a1d754af5e26c | |
| parent | 4150005566bec7827ce1cdd759a2397d47eba583 (diff) | |
| parent | e6ec758b364fcaf9fda35e56740c3fcd7e8fe25e (diff) | |
Merge remote-tracking branch 'javispedro/js-testing'
Conflicts:
daemon/daemon.pro
daemon/dbusconnector.cpp
daemon/manager.cpp
daemon/watchcommands.cpp
daemon/watchcommands.h
daemon/watchconnector.cpp
daemon/watchconnector.h
log4qt-debug.conf
log4qt-release.conf
rpm/pebble.spec
rpm/pebble.yaml
59 files changed, 6924 insertions, 706 deletions
@@ -23,6 +23,8 @@ Features * Transliterate strings to plain ASCII * daemon management app * "org.pebbled" DBus interface +* PebbleKit JS application partial support + (including Pebble object, XMLHTTPRequest, localStorage, geolocation) diff --git a/app/app.pro b/app/app.pro index cb4c33b..97c6232 100644 --- a/app/app.pro +++ b/app/app.pro @@ -3,23 +3,34 @@ 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 OTHER_FILES += \ qml/cover/CoverPage.qml \ qml/pages/ManagerPage.qml \ qml/pages/WatchPage.qml \ qml/pages/AboutPage.qml \ + qml/pages/InstallAppDialog.qml \ + qml/pages/AppConfigDialog.qml \ + qml/pages/WebItemSelDialog.qml \ qml/pebble.qml \ qml/images/* \ + translations/*.ts \ pebble.desktop \ pebble.png + +CONFIG += sailfishapp_i18n +TRANSLATIONS += translations/pebble-es.ts 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 05ca614..c978dd0 100644 --- a/app/pebbledinterface.cpp +++ b/app/pebbledinterface.cpp @@ -1,33 +1,42 @@ #include "pebbledinterface.h" +#include "watch_interface.h" -QString PebbledInterface::PEBBLED_SYSTEMD_UNIT("pebbled.service"); -QString PebbledInterface::PEBBLED_DBUS_SERVICE("org.pebbled"); -QString PebbledInterface::PEBBLED_DBUS_PATH("/"); -QString PebbledInterface::PEBBLED_DBUS_IFACE("org.pebbled"); - -#define PebbledDbusInterface QDBusInterface(PEBBLED_DBUS_SERVICE, PEBBLED_DBUS_PATH, PEBBLED_DBUS_IFACE) - +static const QString PEBBLED_SYSTEMD_UNIT("pebbled.service"); +static const QString PEBBLED_DBUS_SERVICE("org.pebbled"); +static const QString PEBBLED_DBUS_PATH("/org/pebbled/Watch"); +static const QString PEBBLED_DBUS_IFACE("org.pebbled.Watch"); PebbledInterface::PebbledInterface(QObject *parent) : - QObject(parent), systemd(0) + QObject(parent), + systemd(new QDBusInterface("org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + QDBusConnection::sessionBus(), this)), + watch(new OrgPebbledWatchInterface(PEBBLED_DBUS_SERVICE, + PEBBLED_DBUS_PATH, + QDBusConnection::sessionBus(), this)) { - QDBusConnection::sessionBus().connect( - PEBBLED_DBUS_SERVICE, PEBBLED_DBUS_PATH, PEBBLED_DBUS_IFACE, - "connectedChanged", this, SIGNAL(connectedChanged())); + connect(watch, &OrgPebbledWatchInterface::NameChanged, + this, &PebbledInterface::nameChanged); + connect(watch, &OrgPebbledWatchInterface::AddressChanged, + this, &PebbledInterface::addressChanged); + connect(watch, &OrgPebbledWatchInterface::ConnectedChanged, + this, &PebbledInterface::connectedChanged); + connect(watch, &OrgPebbledWatchInterface::AppUuidChanged, + this, &PebbledInterface::appUuidChanged); + connect(watch, &OrgPebbledWatchInterface::AppSlotsChanged, + this, &PebbledInterface::refreshAppSlots); + connect(watch, &OrgPebbledWatchInterface::AllAppsChanged, + this, &PebbledInterface::refreshAllApps); - QDBusConnection::sessionBus().connect( - PEBBLED_DBUS_SERVICE, PEBBLED_DBUS_PATH, PEBBLED_DBUS_IFACE, - "pebbleChanged", this, SLOT(onPebbleChanged())); + connect(watch, &OrgPebbledWatchInterface::ConnectedChanged, + this, &PebbledInterface::onWatchConnectedChanged); // simulate connected change on active changed // as the daemon might not had a chance to send 'connectedChanged' // when going down - connect(this, SIGNAL(activeChanged()), SIGNAL(connectedChanged())); - - systemd = new QDBusInterface("org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - QDBusConnection::sessionBus(), this); + connect(this, &PebbledInterface::activeChanged, + this, &PebbledInterface::connectedChanged); systemd->call("Subscribe"); @@ -44,6 +53,11 @@ PebbledInterface::PebbledInterface(QObject *parent) : } else { qWarning() << unit.error().message(); } + + if (watch->isValid()) { + refreshAllApps(); + refreshAppSlots(); + } } void PebbledInterface::getUnitProperties() @@ -55,9 +69,9 @@ void PebbledInterface::getUnitProperties() QDBusReply<QVariantMap> reply = QDBusConnection::sessionBus().call(request); if (reply.isValid()) { QVariantMap newProperties = reply.value(); - bool emitEnabledChanged = (properties["UnitFileState"] != newProperties["UnitFileState"]); - bool emitActiveChanged = (properties["ActiveState"] != newProperties["ActiveState"]); - properties = newProperties; + bool emitEnabledChanged = (unitProperties["UnitFileState"] != newProperties["UnitFileState"]); + bool emitActiveChanged = (unitProperties["ActiveState"] != newProperties["ActiveState"]); + unitProperties = newProperties; if (emitEnabledChanged) emit enabledChanged(); if (emitActiveChanged) emit activeChanged(); } else { @@ -67,24 +81,16 @@ void PebbledInterface::getUnitProperties() void PebbledInterface::onPropertiesChanged(QString interface, QMap<QString,QVariant> changed, QStringList invalidated) { - qDebug() << __FUNCTION__ << interface << changed << invalidated; + qDebug() << Q_FUNC_INFO << interface << changed << invalidated; if (interface != "org.freedesktop.systemd1.Unit") return; - if (invalidated.contains("UnitFileState") or invalidated.contains("ActiveState")) + if (invalidated.contains("UnitFileState") || invalidated.contains("ActiveState")) getUnitProperties(); } -void PebbledInterface::onPebbleChanged() -{ - qDebug() << __FUNCTION__; - emit nameChanged(); - emit addressChanged(); - emit pebbleChanged(); -} - bool PebbledInterface::enabled() const { - qDebug() << __FUNCTION__; - return properties["UnitFileState"].toString() == "enabled"; + qDebug() << Q_FUNC_INFO; + return unitProperties["UnitFileState"].toString() == "enabled"; } void PebbledInterface::setEnabled(bool enabled) @@ -103,8 +109,8 @@ void PebbledInterface::setEnabled(bool enabled) bool PebbledInterface::active() const { - qDebug() << __FUNCTION__; - return properties["ActiveState"].toString() == "active"; + qDebug() << Q_FUNC_INFO; + return unitProperties["ActiveState"].toString() == "active"; } void PebbledInterface::setActive(bool active) @@ -118,48 +124,199 @@ void PebbledInterface::setActive(bool active) bool PebbledInterface::connected() const { - qDebug() << __FUNCTION__; - return PebbledDbusInterface.property(__FUNCTION__).toBool(); + qDebug() << Q_FUNC_INFO; + return watch->connected(); } -QVariantMap PebbledInterface::pebble() const +QString PebbledInterface::name() const { - qDebug() << __FUNCTION__; - return PebbledDbusInterface.property(__FUNCTION__).toMap(); + qDebug() << Q_FUNC_INFO; + return watch->name(); } -QString PebbledInterface::name() const +QString PebbledInterface::address() const { - qDebug() << __FUNCTION__; - return PebbledDbusInterface.property(__FUNCTION__).toString(); + qDebug() << Q_FUNC_INFO; + return watch->address(); } -QString PebbledInterface::address() const +QString PebbledInterface::appUuid() const { - qDebug() << __FUNCTION__; - return PebbledDbusInterface.property(__FUNCTION__).toString(); + qDebug() << Q_FUNC_INFO; + return watch->appUuid(); } void PebbledInterface::ping() { - qDebug() << __FUNCTION__; - PebbledDbusInterface.call("ping", 66); + qDebug() << Q_FUNC_INFO; + watch->Ping(66); } void PebbledInterface::time() { - qDebug() << __FUNCTION__; - PebbledDbusInterface.call("time"); + qDebug() << Q_FUNC_INFO; + watch->SyncTime(); } void PebbledInterface::disconnect() { - qDebug() << __FUNCTION__; - PebbledDbusInterface.call("disconnect"); + qDebug() << Q_FUNC_INFO; + watch->Disconnect(); } void PebbledInterface::reconnect() { - qDebug() << __FUNCTION__; - PebbledDbusInterface.call("reconnect"); + qDebug() << Q_FUNC_INFO; + watch->Reconnect(); +} + +QUrl PebbledInterface::configureApp(const QString &uuid) +{ + qDebug() << Q_FUNC_INFO << uuid; + QDBusPendingReply<QString> reply = watch->StartAppConfiguration(uuid); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << "Received error:" << reply.error().message(); + return QUrl(); + } else { + return QUrl(reply.value()); + } +} + +bool PebbledInterface::isAppInstalled(const QString &uuid) const +{ + QUuid u(uuid); + + foreach (const QString &s, _appSlots) { + if (QUuid(s) == u) { + return true; + } + } + + 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; + watch->SendAppConfigurationData(uuid, data); +} + +void PebbledInterface::launchApp(const QString &uuid) +{ + qDebug() << Q_FUNC_INFO << uuid; + QDBusPendingReply<> reply = watch->LaunchApp(uuid); + reply.waitForFinished(); + + // 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 (QUuid(watch->appUuid()) != u && sleep_count < 5) { + qDebug() << "Waiting for" << u.toString() << "to launch"; + QThread::sleep(1); + sleep_count++; + } +} + +void PebbledInterface::uploadApp(const QString &uuid, int slot) +{ + qDebug() << Q_FUNC_INFO << uuid << slot; + QDBusPendingReply<> reply = watch->UploadApp(uuid, slot); + reply.waitForFinished(); +} + +void PebbledInterface::unloadApp(int slot) +{ + qDebug() << Q_FUNC_INFO << slot; + QDBusPendingReply<> reply = watch->UnloadApp(slot); + reply.waitForFinished(); +} + +QStringList PebbledInterface::appSlots() const +{ + return _appSlots; +} + +QVariantList PebbledInterface::allApps() const +{ + return _apps; +} + +QVariantMap PebbledInterface::appInfoByUuid(const QString &uuid) const +{ + int index = _appsByUuid.value(QUuid(uuid), -1); + if (index >= 0) { + return _apps[index].toMap(); + } else { + return QVariantMap(); + } +} + +void PebbledInterface::onWatchConnectedChanged() +{ + qDebug() << Q_FUNC_INFO; + if (watch->connected()) { + refreshAllApps(); + refreshAppSlots(); + } +} + +void PebbledInterface::refreshAppSlots() +{ + qDebug() << "refreshing app slots list"; + _appSlots = watch->appSlots(); + emit appSlotsChanged(); +} + +void PebbledInterface::refreshAllApps() +{ + _apps.clear(); + _appsByUuid.clear(); + _appMenuIcons.clear(); + + qDebug() << "refreshing all apps list"; + + const QVariantList l = watch->allApps(); + foreach (const QVariant &v, l) { + QVariantMap orig = qdbus_cast<QVariantMap>(v.value<QDBusArgument>()); + QUuid uuid = orig.value("uuid").toUuid(); + if (uuid.isNull()) { + qWarning() << "Invalid app uuid received" << orig; + continue; + } + + QVariantMap m; + m.insert("uuid", uuid.toString()); + 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)); + } + + std::sort(_apps.begin(), _apps.end(), [](const QVariant &v1, const QVariant &v2) { + const QVariantMap &a = v1.toMap(); + const QVariantMap &b = v2.toMap(); + return a.value("shortName").toString() < b.value("shortName").toString(); + }); + + for (int i = 0; i < _apps.size(); ++i) { + QUuid uuid = _apps[i].toMap().value("uuid").toUuid(); + _appsByUuid.insert(uuid, i); + } + + qDebug() << _appsByUuid.size() << "different app uuids known"; + + emit allAppsChanged(); } diff --git a/app/pebbledinterface.h b/app/pebbledinterface.h index 0a6f15d..51efa12 100644 --- a/app/pebbledinterface.h +++ b/app/pebbledinterface.h @@ -2,48 +2,57 @@ #define PEBBLEDINTERFACE_H #include <QObject> -#include <QtDBus/QtDBus> -#include <QDBusArgument> +#include <QUrl> +#include <QHash> +#include <QUuid> +#include <QImage> +#include <QDBusInterface> + +class OrgPebbledWatchInterface; class PebbledInterface : public QObject { Q_OBJECT + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString address READ address NOTIFY addressChanged) + Q_PROPERTY(QString appUuid READ appUuid NOTIFY appUuidChanged) - static QString PEBBLED_SYSTEMD_UNIT; - static QString PEBBLED_DBUS_SERVICE; - static QString PEBBLED_DBUS_PATH; - static QString PEBBLED_DBUS_IFACE; + Q_PROPERTY(QStringList appSlots READ appSlots NOTIFY appSlotsChanged) + Q_PROPERTY(QVariantList allApps READ allApps NOTIFY allAppsChanged) - Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) - bool enabled() const; +public: + explicit PebbledInterface(QObject *parent = 0); - Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + bool enabled() const; bool active() const; - - Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) bool connected() const; + QString name() const; + QString address() const; + QString appUuid() const; - Q_PROPERTY(QVariantMap pebble READ pebble NOTIFY pebbleChanged) - QVariantMap pebble() const; + QStringList appSlots() const; + QVariantList allApps() const; - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - QString name() const; + Q_INVOKABLE QVariantMap appInfoByUuid(const QString& uuid) const; - Q_PROPERTY(QString address READ address NOTIFY addressChanged) - QString address() const; + Q_INVOKABLE QUrl configureApp(const QString &uuid); + Q_INVOKABLE bool isAppInstalled(const QString &uuid) const; -public: - explicit PebbledInterface(QObject *parent = 0); + QImage menuIconForApp(const QUuid &uuid) const; signals: void enabledChanged(); void activeChanged(); - void connectedChanged(); - void pebbleChanged(); void nameChanged(); void addressChanged(); + void appUuidChanged(); + void appSlotsChanged(); + void allAppsChanged(); public slots: void setEnabled(bool); @@ -53,16 +62,30 @@ public slots: void disconnect(); void reconnect(); + void setAppConfiguration(const QString &uuid, const QString &data); + + void launchApp(const QString &uuid); + void uploadApp(const QString &uuid, int slot); + void unloadApp(int slot); + private slots: + void onWatchConnectedChanged(); void getUnitProperties(); void onPropertiesChanged(QString interface, QMap<QString, QVariant> changed, QStringList invalidated); - void onPebbleChanged(); + void refreshAppSlots(); + void refreshAllApps(); private: QDBusInterface *systemd; + OrgPebbledWatchInterface *watch; QDBusObjectPath unitPath; + QVariantMap unitProperties; - QVariantMap properties; + // Cached properties + QStringList _appSlots; + QVariantList _apps; + QHash<QUuid, int> _appsByUuid; + QHash<QUuid, QImage> _appMenuIcons; }; #endif // PEBBLEDINTERFACE_H diff --git a/app/qml/pages/AboutPage.qml b/app/qml/pages/AboutPage.qml index 3ab92a0..12f4d53 100644 --- a/app/qml/pages/AboutPage.qml +++ b/app/qml/pages/AboutPage.qml @@ -40,7 +40,7 @@ Page { anchors { left: parent.left right: parent.right - margins: Theme.paddingSmall + margins: Theme.paddingMedium } font.pixelSize: Theme.fontSizeTiny horizontalAlignment: Text.AlignJustify diff --git a/app/qml/pages/AppConfigDialog.qml b/app/qml/pages/AppConfigDialog.qml new file mode 100644 index 0000000..1562985 --- /dev/null +++ b/app/qml/pages/AppConfigDialog.qml @@ -0,0 +1,87 @@ +import QtQuick 2.0 +import QtQml 2.1 +import QtWebKit 3.0 +import Sailfish.Silica 1.0 + +Dialog { + id: appConfigPage + + property alias url: webview.url + property string uuid + property string name + + SilicaWebView { + id: webview + visible: url != "" + anchors.fill: parent + + header: DialogHeader { + title: "Configuring " + name + } + + VerticalScrollDecorator { flickable: webview } + + overridePageStackNavigation: true + + onNavigationRequested: { + console.log("appconfig navigation requested to " + request.url); + var url = request.url.toString(); + if (/^pebblejs:\/\/close/.exec(url)) { + var data = decodeURIComponent(url.substring(17)); + console.log("appconfig requesting close; data: " + data); + pebbled.setAppConfiguration(uuid, data); + appConfigPage.canAccept = true; + appConfigPage.accept(); + request.action = WebView.IgnoreRequest; + } else { + request.action = WebView.AcceptRequest; + } + } + + experimental.itemSelector: Component { + Item { + Component.onCompleted: { + var dialog = pageStack.push(Qt.resolvedUrl("WebItemSelDialog.qml"), { + model: model.items + }); + dialog.onRejected.connect(function() { + model.reject(); + }); + dialog.onAccepted.connect(function() { + model.accept(dialog.selectedIndex); + }); + } + } + } + } + + ProgressBar { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + visible: webview.visible && webview.loading + minimumValue: 0 + maximumValue: 100 + indeterminate: webview.loadProgress === 0 + value: webview.loadProgress + } + + 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 + } + + canAccept: false +} diff --git a/app/qml/pages/InstallAppDialog.qml b/app/qml/pages/InstallAppDialog.qml new file mode 100644 index 0000000..fa96c28 --- /dev/null +++ b/app/qml/pages/InstallAppDialog.qml @@ -0,0 +1,75 @@ +import QtQuick 2.0 +import QtQml 2.1 +import Sailfish.Silica 1.0 + +Dialog { + id: installAppPage + + property string selectedUuid; + + SilicaListView { + id: appList + anchors.fill: parent + + header: DialogHeader { + title: qsTr("Install app") + defaultAcceptText: qsTr("Install") + } + + VerticalScrollDecorator { flickable: flickable } + + currentIndex: -1 + + delegate: ListItem { + id: appDelegate + contentHeight: Theme.itemSizeSmall + + property string uuid: modelData.uuid + property bool alreadyInstalled: pebbled.isAppInstalled(uuid) + + Item { + id: appIcon + width: Theme.itemSizeSmall + height: Theme.itemSizeSmall + + anchors { + top: parent.top + left: parent.left + leftMargin: Theme.paddingLarge + } + + Image { + id: appImage + anchors.centerIn: parent + source: "image://pebble-app-icon/" + uuid; + scale: 2 + } + } + + Label { + id: appName + anchors { + left: appIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + text: modelData.longName + color: appDelegate.highlighted ? Theme.highlightColor : Theme.primaryColor + } + + onClicked: { + appList.currentIndex = index + if (!alreadyInstalled) { + selectedUuid = uuid + accept(); + } + } + } + + model: pebbled.allApps + } + + canAccept: appList.currentIndex >= 0 && !appList.currentItem.alreadyInstalled +} diff --git a/app/qml/pages/WatchPage.qml b/app/qml/pages/WatchPage.qml index 90e5ec9..43c2b99 100644 --- a/app/qml/pages/WatchPage.qml +++ b/app/qml/pages/WatchPage.qml @@ -34,7 +34,7 @@ import QtQml 2.1 import Sailfish.Silica 1.0 Page { - id: page + id: watchPage SilicaFlickable { id: flickable @@ -45,9 +45,8 @@ Page { Column { id: column + width: watchPage.width - width: page.width - spacing: Theme.paddingLarge PageHeader { title: pebbled.name } @@ -61,7 +60,7 @@ Page { Button { - text: "Ping" + text: qsTr("Ping") width: parent.width / 2 onClicked: { pebbled.ping(66) @@ -69,7 +68,7 @@ Page { } Button { - text: "Sync Time" + text: qsTr("Sync Time") width: parent.width / 2 onClicked: { pebbled.time() @@ -77,6 +76,154 @@ Page { } } + Item { + width: parent.width + height: Theme.paddingLarge + } + + Label { + text: qsTr("Installed applications") + font.family: Theme.fontFamilyHeading + color: Theme.highlightColor + anchors.right: parent.right + anchors.rightMargin: Theme.paddingMedium + } + + Repeater { + id: slotsRepeater + model: pebbled.appSlots + + ListItem { + id: slotDelegate + menu: slotMenu + contentHeight: Theme.itemSizeSmall + + property bool isEmptySlot: modelData === "" + property var appInfo: pebbled.appInfoByUuid(modelData) + property bool isKnownApp: appInfo.hasOwnProperty("uuid") + property bool busy: false + + function configure() { + var uuid = modelData; + pebbled.launchApp(uuid); + console.log("going to call configure on app with uuid " + uuid); + var url = pebbled.configureApp(uuid); + console.log("received url: " + url); + pageStack.push(Qt.resolvedUrl("AppConfigDialog.qml"), { + url: url, + uuid: uuid, + name: appInfo.longName + }); + } + + function remove() { + remorseAction(qsTr("Uninstalling"), function() { + busy = true; + pebbled.unloadApp(index); + }); + } + + function install() { + var dialog = pageStack.push(Qt.resolvedUrl("InstallAppDialog.qml")); + dialog.accepted.connect(function() { + var uuid = dialog.selectedUuid; + + if (pebbled.isAppInstalled(uuid)) { + console.warn("uuid already installed"); + return; + } + + var slot = index; + console.log("installing " + uuid + " into " + slot); + busy = true; + pebbled.uploadApp(uuid, slot); + }); + + } + + Item { + id: slotIcon + width: Theme.itemSizeSmall + height: Theme.itemSizeSmall + + anchors { + top: parent.top + left: parent.left + leftMargin: Theme.paddingLarge + } + + 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: slotIcon.right + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.paddingLarge + verticalCenter: parent.verticalCenter + } + text: isEmptySlot ? qsTr("(empty slot)") : (isKnownApp ? appInfo.longName : qsTr("(slot in use by unknown app)")) + color: slotDelegate.highlighted ? Theme.highlightColor : Theme.primaryColor + onTextChanged: slotDelegate.busy = false; + } + + Component { + id: slotMenu + ContextMenu { + MenuItem { + text: qsTr("Install app...") + visible: isEmptySlot + onClicked: install(); + } + MenuItem { + text: qsTr("Configure...") + visible: !isEmptySlot && isKnownApp + onClicked: configure(); + } + MenuItem { + text: qsTr("Uninstall") + visible: !isEmptySlot + onClicked: remove(); + } + } + } + + onClicked: { + if (isEmptySlot) { + install(); + } else { + showMenu(); + } + } + } + } } } } diff --git a/app/qml/pages/WebItemSelDialog.qml b/app/qml/pages/WebItemSelDialog.qml new file mode 100644 index 0000000..f8c49f2 --- /dev/null +++ b/app/qml/pages/WebItemSelDialog.qml @@ -0,0 +1,45 @@ +import QtQuick 2.0 +import QtQml 2.1 +import Sailfish.Silica 1.0 + +Dialog { + id: itemSelDialog + property alias model: listView.model + property int selectedIndex: -1 + + SilicaListView { + id: listView + anchors.fill: parent + + VerticalScrollDecorator { flickable: webview } + + header: PageHeader { + } + + delegate: ListItem { + id: itemDelegate + contentHeight: Theme.itemSizeSmall + + Label { + anchors { + left: parent.left + leftMargin: Theme.paddingMedium + right: parent.right + rightMargin: Theme.paddingMedium + verticalCenter: parent.verticalCenter + } + text: model.text + color: model.enabled ? + (itemDelegate.highlighted ? Theme.highlightColor : Theme.primaryColor) + : Theme.secondaryColor + truncationMode: TruncationMode.Fade + } + + enabled: model.enabled + onClicked: { + selectedIndex = model.index; + accept(); + } + } + } +} 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/app/translations/pebble-es.ts b/app/translations/pebble-es.ts new file mode 100644 index 0000000..0316ecb --- /dev/null +++ b/app/translations/pebble-es.ts @@ -0,0 +1,254 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="es_ES"> +<context> + <name>AboutPage</name> + <message> + <location filename="../qml/pages/AboutPage.qml" line="24"/> + <source>Version </source> + <translation>Versión </translation> + </message> + <message> + <location filename="../qml/pages/AboutPage.qml" line="47"/> + <source>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/AboutPage.qml" line="71"/> + <source>Bugs?</source> + <translation>¿Errores?</translation> + </message> +</context> +<context> + <name>AppConfigDialog</name> + <message> + <location filename="../qml/pages/AppConfigDialog.qml" line="75"/> + <source>No configuration settings available</source> + <translation>No hay opciones disponibles para configurar</translation> + </message> +</context> +<context> + <name>CoverPage</name> + <message> + <location filename="../qml/cover/CoverPage.qml" line="56"/> + <source>connected</source> + <translation>conectado</translation> + </message> + <message> + <location filename="../qml/cover/CoverPage.qml" line="56"/> + <source>disconnected</source> + <translation>desconectado</translation> + </message> +</context> +<context> + <name>InstallAppDialog</name> + <message> + <location filename="../qml/pages/InstallAppDialog.qml" line="15"/> + <source>Install app</source> + <translation>Instalar app</translation> + </message> + <message> + <location filename="../qml/pages/InstallAppDialog.qml" line="16"/> + <source>Install</source> + <translation>Instalar</translation> + </message> +</context> +<context> + <name>ManagerPage</name> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="65"/> + <source>About</source> + <translation>Acerca de</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="76"/> + <source>Pebble Manager</source> + <translation></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="83"/> + <source>Waiting for watch... +If it can't be found please check it's available and paired in Bluetooth settings.</source> + <translation>Buscando el reloj +Si esto tarda mucho, comprueba que el reloj esté emparejado correctamente.</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="104"/> + <source>Service</source> + <translation>Servicio</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="111"/> + <source>Enabled</source> + <translation>Habilitado</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="112"/> + <source>Automatic startup</source> + <translation>Inicio automático</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="112"/> + <source>Manual startup</source> + <translation>Inicio manual</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="118"/> + <source>Active</source> + <translation>Activo</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="119"/> + <source>Running</source> + <translation>Ejecutándose</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="119"/> + <source>Dead</source> + <translation>Detenido</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="125"/> + <source>Connection</source> + <translation>Conexión</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="126"/> + <source>Connected</source> + <translation>Conectado</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="126"/> + <source>Disconnected</source> + <translation>Desconectado</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="139"/> + <source>Settings</source> + <translation>Configuración</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="146"/> + <source>Forward phone calls</source> + <translation>Transferir llamadas</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="154"/> + <source>Silent when connected</source> + <translation>Modo silencio automático</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="155"/> + <source>Sets phone profile to "silent" when Pebble is connected</source> + <translation>Activa el modo silencio cuando se conecte un Pebble</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="163"/> + <source>Transliterate messages</source> + <translation>Transliterar mensajes</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="164"/> + <source>Messages are transliterated to ASCII before sending to Pebble</source> + <translation>Codifica los mensajes entrates a ASCII antes de enviarlos a Pebble</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="173"/> + <source>Notifications</source> + <translation>Notificaciones</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="181"/> + <source>Messaging</source> + <translation>MensajerÃa</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="182"/> + <source>SMS and IM</source> + <translation>SMS y chat</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="191"/> + <source>Missed call</source> + <translation>Llamadas perdidas</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="200"/> + <source>Emails</source> + <translation>Correos electrónicos</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="209"/> + <source>Mitakuuluu</source> + <translation></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="218"/> + <source>Twitter</source> + <translation>Twitter</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="228"/> + <source>Facebook</source> + <translation>Facebook</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="237"/> + <source>Other notifications</source> + <translation>Resto de notificaciones</translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="246"/> + <source>All notifications</source> + <translation>Todas las notificaciones</translation> + </message> +</context> +<context> + <name>WatchPage</name> + <message> + <location filename="../qml/pages/WatchPage.qml" line="63"/> + <source>Ping</source> + <translation>Ping</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="71"/> + <source>Sync Time</source> + <translation>Ajustar hora</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="85"/> + <source>Installed applications</source> + <translation>Aplicaciones instaladas</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="120"/> + <source>Uninstalling</source> + <translation>Desinstalando</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="192"/> + <source>(empty slot)</source> + <translation>(hueco libre)</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="192"/> + <source>(slot in use by unknown app)</source> + <translation>(hueco en uso)</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="201"/> + <source>Install app...</source> + <translation>Instalar app...</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="206"/> + <source>Configure...</source> + <translation>Configurar...</translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="211"/> + <source>Uninstall</source> + <translation>Desinstalar</translation> + </message> +</context> +</TS> diff --git a/app/translations/pebble.ts b/app/translations/pebble.ts new file mode 100644 index 0000000..7a78d9d --- /dev/null +++ b/app/translations/pebble.ts @@ -0,0 +1,253 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1"> +<context> + <name>AboutPage</name> + <message> + <location filename="../qml/pages/AboutPage.qml" line="24"/> + <source>Version </source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/AboutPage.qml" line="47"/> + <source>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/AboutPage.qml" line="71"/> + <source>Bugs?</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>AppConfigDialog</name> + <message> + <location filename="../qml/pages/AppConfigDialog.qml" line="75"/> + <source>No configuration settings available</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>CoverPage</name> + <message> + <location filename="../qml/cover/CoverPage.qml" line="56"/> + <source>connected</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/cover/CoverPage.qml" line="56"/> + <source>disconnected</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>InstallAppDialog</name> + <message> + <location filename="../qml/pages/InstallAppDialog.qml" line="15"/> + <source>Install app</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/InstallAppDialog.qml" line="16"/> + <source>Install</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>ManagerPage</name> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="65"/> + <source>About</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="76"/> + <source>Pebble Manager</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="83"/> + <source>Waiting for watch... +If it can't be found please check it's available and paired in Bluetooth settings.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="104"/> + <source>Service</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="111"/> + <source>Enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="112"/> + <source>Automatic startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="112"/> + <source>Manual startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="118"/> + <source>Active</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="119"/> + <source>Running</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="119"/> + <source>Dead</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="125"/> + <source>Connection</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="126"/> + <source>Connected</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="126"/> + <source>Disconnected</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="139"/> + <source>Settings</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="146"/> + <source>Forward phone calls</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="154"/> + <source>Silent when connected</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="155"/> + <source>Sets phone profile to "silent" when Pebble is connected</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="163"/> + <source>Transliterate messages</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="164"/> + <source>Messages are transliterated to ASCII before sending to Pebble</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="173"/> + <source>Notifications</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="181"/> + <source>Messaging</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="182"/> + <source>SMS and IM</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="191"/> + <source>Missed call</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="200"/> + <source>Emails</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="209"/> + <source>Mitakuuluu</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="218"/> + <source>Twitter</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="228"/> + <source>Facebook</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="237"/> + <source>Other notifications</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/ManagerPage.qml" line="246"/> + <source>All notifications</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>WatchPage</name> + <message> + <location filename="../qml/pages/WatchPage.qml" line="63"/> + <source>Ping</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="71"/> + <source>Sync Time</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="85"/> + <source>Installed applications</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="120"/> + <source>Uninstalling</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="192"/> + <source>(empty slot)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="192"/> + <source>(slot in use by unknown app)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="201"/> + <source>Install app...</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="206"/> + <source>Configure...</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="../qml/pages/WatchPage.qml" line="211"/> + <source>Uninstall</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/daemon/appinfo.cpp b/daemon/appinfo.cpp new file mode 100644 index 0000000..4397abc --- /dev/null +++ b/daemon/appinfo.cpp @@ -0,0 +1,188 @@ +#include <QSharedData> +#include <QBuffer> +#include "appinfo.h" + +struct AppInfoData : public QSharedData { + QUuid uuid; + QString shortName; + QString longName; + QString companyName; + int versionCode; + QString versionLabel; + bool watchface; + bool jskit; + AppInfo::Capabilities capabilities; + QHash<QString, int> keyInts; + QHash<int, QString> keyNames; + QImage menuIcon; + QString path; +}; + +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) +{ +} + +AppInfo &AppInfo::operator=(const AppInfo &rhs) +{ + if (this != &rhs) + d.operator=(rhs.d); + return *this; +} + +AppInfo::~AppInfo() +{ +} + +QUuid AppInfo::uuid() const +{ + return d->uuid; +} + +void AppInfo::setUuid(const QUuid &uuid) +{ + d->uuid = uuid; +} + +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); + d->keyNames.insert(value, key); +} + +bool AppInfo::hasAppKeyValue(int value) const +{ + return d->keyNames.contains(value); +} + +QString AppInfo::appKeyForValue(int value) const +{ + return d->keyNames.value(value); +} + +bool AppInfo::hasAppKey(const QString &key) const +{ + return d->keyInts.contains(key); +} + +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; +} + +void AppInfo::setPath(const QString &string) +{ + d->path = string; +} diff --git a/daemon/appinfo.h b/daemon/appinfo.h new file mode 100644 index 0000000..3d5c4b4 --- /dev/null +++ b/daemon/appinfo.h @@ -0,0 +1,86 @@ +#ifndef APPINFO_H +#define APPINFO_H + +#include <QSharedDataPointer> +#include <QUuid> +#include <QHash> +#include <QImage> + +class AppInfoData; + +class AppInfo +{ + Q_GADGET + +public: + enum Capability { + Location = 1 << 0, + Configurable = 1 << 2 + }; + Q_DECLARE_FLAGS(Capabilities, Capability) + + Q_PROPERTY(QUuid uuid READ uuid WRITE setUuid) + Q_PROPERTY(QString shortName READ shortName WRITE setShortName) + Q_PROPERTY(QString longName READ longName WRITE setLongName) + Q_PROPERTY(QString companyName READ companyName WRITE setCompanyName) + Q_PROPERTY(int versionCode READ versionCode WRITE setVersionCode) + Q_PROPERTY(QString versionLabel READ versionLabel WRITE setVersionLabel) + 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: + AppInfo(); + AppInfo(const AppInfo &); + AppInfo &operator=(const AppInfo &); + ~AppInfo(); + + QUuid uuid() const; + void setUuid(const QUuid &uuid); + + QString shortName() const; + void setShortName(const QString &string); + + QString longName() const; + void setLongName(const QString &string); + + QString companyName() const; + void setCompanyName(const QString &string); + + int versionCode() const; + void setVersionCode(int code); + + QString versionLabel() const; + void setVersionLabel(const QString &string); + + bool isWatchface() const; + void setWatchface(bool b); + + bool isJSKit() const; + void setJSKit(bool b); + + Capabilities capabilities() const; + void setCapabilities(Capabilities caps); + + void addAppKey(const QString &key, int value); + + bool hasAppKeyValue(int value) const; + QString appKeyForValue(int value) const; + + 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); + +private: + QSharedDataPointer<AppInfoData> d; +}; + +#endif // APPINFO_H diff --git a/daemon/appmanager.cpp b/daemon/appmanager.cpp new file mode 100644 index 0000000..c10cf22 --- /dev/null +++ b/daemon/appmanager.cpp @@ -0,0 +1,267 @@ +#include <QStandardPaths> +#include <QJsonDocument> +#include <QJsonObject> +#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), l(metaObject()->className()), + _watcher(new QFileSystemWatcher(this)) +{ + connect(_watcher, &QFileSystemWatcher::directoryChanged, + this, &AppManager::rescan); + + QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); + if (!dataDir.exists("apps")) { + if (!dataDir.mkdir("apps")) { + logger()->warn() << "could not create 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<QUuid> 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::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.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; + } + + _apps.insert(info.uuid(), info); + _names.insert(info.shortName(), info.uuid()); + + const char *type = info.isWatchface() ? "watchface" : "app"; + qCDebug(l) << "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)) { + 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; +} + +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 new file mode 100644 index 0000000..e96ffe5 --- /dev/null +++ b/daemon/appmanager.h @@ -0,0 +1,42 @@ +#ifndef APPMANAGER_H +#define APPMANAGER_H + +#include <QObject> +#include <QHash> +#include <QUuid> +#include <QFileSystemWatcher> +#include <QLoggingCategory> +#include "appinfo.h" + +class AppManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit AppManager(QObject *parent = 0); + + QStringList appPaths() const; + QList<QUuid> appUuids() const; + + AppInfo info(const QUuid &uuid) const; + AppInfo info(const QString &shortName) const; + +public slots: + void rescan(); + +signals: + void appsChanged(); + +private: + void scanApp(const QString &path); + QByteArray extractFromResourcePack(const QString &file, int id) const; + QImage decodeResourceImage(const QByteArray &data) const; + +private: + QFileSystemWatcher *_watcher; + QHash<QUuid, AppInfo> _apps; + QHash<QString, QUuid> _names; +}; + +#endif // APPMANAGER_H diff --git a/daemon/appmsgmanager.cpp b/daemon/appmsgmanager.cpp new file mode 100644 index 0000000..1a4a424 --- /dev/null +++ b/daemon/appmsgmanager.cpp @@ -0,0 +1,389 @@ +#include <QTimer> + +#include "appmsgmanager.h" +#include "unpacker.h" +#include "packer.h" + +// TODO D-Bus server for non JS kit apps!!!! + +AppMsgManager::AppMsgManager(AppManager *apps, WatchConnector *watch, QObject *parent) + : QObject(parent), l(metaObject()->className()), apps(apps), + watch(watch), _lastTransactionId(0), _timeout(new QTimer(this)) +{ + connect(watch, &WatchConnector::connectedChanged, + this, &AppMsgManager::handleWatchConnectedChanged); + + _timeout->setSingleShot(true); + _timeout->setInterval(3000); + connect(_timeout, &QTimer::timeout, + this, &AppMsgManager::handleTimeout); + + watch->setEndpointHandler(WatchConnector::watchLAUNCHER, + [this](const QByteArray &data) { + switch (data.at(0)) { + case WatchConnector::appmsgPUSH: + handleLauncherPushMessage(data); + break; + case WatchConnector::appmsgACK: + case WatchConnector::appmsgNACK: + // TODO we ignore those for now. + break; + } + + return true; + }); + + watch->setEndpointHandler(WatchConnector::watchAPPLICATION_MESSAGE, + [this](const QByteArray &data) { + switch (data.at(0)) { + case WatchConnector::appmsgPUSH: + handlePushMessage(data); + break; + case WatchConnector::appmsgACK: + handleAckMessage(data, true); + break; + case WatchConnector::appmsgNACK: + handleAckMessage(data, false); + break; + default: + qCWarning(l) << "Unknown application message type:" << int(data.at(0)); + break; + } + + return true; + }); +} + +void AppMsgManager::send(const QUuid &uuid, const QVariantMap &data, const std::function<void ()> &ackCallback, const std::function<void ()> &nackCallback) +{ + PendingTransaction trans; + trans.uuid = uuid; + trans.transactionId = ++_lastTransactionId; + trans.dict = mapAppKeys(uuid, data); + trans.ackCallback = ackCallback; + trans.nackCallback = nackCallback; + + qCDebug(l) << "Queueing appmsg" << trans.transactionId << "to" << trans.uuid + << "with dict" << trans.dict; + + _pending.enqueue(trans); + if (_pending.size() == 1) { + // This is the only transaction on the queue + // Therefore, we were idle before: we can submit this transaction right now. + transmitNextPendingTransaction(); + } +} + +void AppMsgManager::setMessageHandler(const QUuid &uuid, MessageHandlerFunc func) +{ + _handlers.insert(uuid, func); +} + +void AppMsgManager::clearMessageHandler(const QUuid &uuid) +{ + _handlers.remove(uuid); +} + +uint AppMsgManager::lastTransactionId() const +{ + return _lastTransactionId; +} + +uint AppMsgManager::nextTransactionId() const +{ + return _lastTransactionId + 1; +} + +void AppMsgManager::send(const QUuid &uuid, const QVariantMap &data) +{ + std::function<void()> nullCallback; + send(uuid, data, nullCallback, nullCallback); +} + +void AppMsgManager::launchApp(const QUuid &uuid) +{ + WatchConnector::Dict dict; + dict.insert(1, WatchConnector::launcherSTARTED); + + qCDebug(l) << "Sending message to launcher" << uuid << dict; + + QByteArray msg = buildPushMessage(++_lastTransactionId, uuid, dict); + watch->sendMessage(WatchConnector::watchLAUNCHER, msg); +} + +void AppMsgManager::closeApp(const QUuid &uuid) +{ + WatchConnector::Dict dict; + dict.insert(1, WatchConnector::launcherSTOPPED); + + qCDebug(l) << "Sending message to launcher" << uuid << dict; + + QByteArray msg = buildPushMessage(++_lastTransactionId, uuid, dict); + watch->sendMessage(WatchConnector::watchLAUNCHER, msg); +} + +WatchConnector::Dict AppMsgManager::mapAppKeys(const QUuid &uuid, const QVariantMap &data) +{ + AppInfo info = apps->info(uuid); + if (info.uuid() != uuid) { + qCWarning(l) << "Unknown app GUID while sending message:" << uuid; + } + + WatchConnector::Dict d; + + for (QVariantMap::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) { + if (info.hasAppKey(it.key())) { + d.insert(info.valueForAppKey(it.key()), it.value()); + } else { + // Even if we do not know about this appkey, try to see if it's already a numeric key we + // can send to the watch. + bool ok = false; + int num = it.key().toInt(&ok); + if (ok) { + d.insert(num, it.value()); + } else { + qCWarning(l) << "Unknown appKey" << it.key() << "for app with GUID" << uuid; + } + } + } + + return d; +} + +QVariantMap AppMsgManager::mapAppKeys(const QUuid &uuid, const WatchConnector::Dict &dict) +{ + AppInfo info = apps->info(uuid); + if (info.uuid() != uuid) { + qCWarning(l) << "Unknown app GUID while sending message:" << uuid; + } + + QVariantMap data; + + for (WatchConnector::Dict::const_iterator it = dict.constBegin(); it != dict.constEnd(); ++it) { + if (info.hasAppKeyValue(it.key())) { + data.insert(info.appKeyForValue(it.key()), it.value()); + } else { + qCWarning(l) << "Unknown appKey value" << it.key() << "for app with GUID" << uuid; + data.insert(QString::number(it.key()), it.value()); + } + } + + return data; +} + +bool AppMsgManager::unpackPushMessage(const QByteArray &msg, quint8 *transaction, QUuid *uuid, WatchConnector::Dict *dict) +{ + Unpacker u(msg); + quint8 code = u.read<quint8>(); + Q_UNUSED(code); + Q_ASSERT(code == WatchConnector::appmsgPUSH); + + *transaction = u.read<quint8>(); + *uuid = u.readUuid(); + *dict = u.readDict(); + + if (u.bad()) { + return false; + } + + return true; +} + +QByteArray AppMsgManager::buildPushMessage(quint8 transaction, const QUuid &uuid, const WatchConnector::Dict &dict) +{ + QByteArray ba; + Packer p(&ba); + p.write<quint8>(WatchConnector::appmsgPUSH); + p.write<quint8>(transaction); + p.writeUuid(uuid); + p.writeDict(dict); + + return ba; +} + +QByteArray AppMsgManager::buildAckMessage(quint8 transaction) +{ + QByteArray ba(2, Qt::Uninitialized); + ba[0] = WatchConnector::appmsgACK; + ba[1] = transaction; + return ba; +} + +QByteArray AppMsgManager::buildNackMessage(quint8 transaction) +{ + QByteArray ba(2, Qt::Uninitialized); + ba[0] = WatchConnector::appmsgNACK; + ba[1] = transaction; + return ba; +} + +void AppMsgManager::handleLauncherPushMessage(const QByteArray &data) +{ + quint8 transaction; + QUuid uuid; + WatchConnector::Dict dict; + + if (!unpackPushMessage(data, &transaction, &uuid, &dict)) { + // Failed to parse! + // Since we're the only one handling this endpoint, + // all messages must be accepted + qCWarning(l) << "Failed to parser LAUNCHER PUSH message"; + return; + } + if (!dict.contains(1)) { + qCWarning(l) << "LAUNCHER message has no item in dict"; + return; + } + + switch (dict.value(1).toInt()) { + case WatchConnector::launcherSTARTED: + qCDebug(l) << "App starting in watch:" << uuid; + this->watch->sendMessage(WatchConnector::watchLAUNCHER, + buildAckMessage(transaction)); + emit appStarted(uuid); + break; + case WatchConnector::launcherSTOPPED: + qCDebug(l) << "App stopping in watch:" << uuid; + this->watch->sendMessage(WatchConnector::watchLAUNCHER, + buildAckMessage(transaction)); + emit appStopped(uuid); + break; + default: + qCWarning(l) << "LAUNCHER pushed unknown message:" << uuid << dict; + this->watch->sendMessage(WatchConnector::watchLAUNCHER, + buildNackMessage(transaction)); + break; + } +} + +void AppMsgManager::handlePushMessage(const QByteArray &data) +{ + quint8 transaction; + QUuid uuid; + WatchConnector::Dict dict; + + if (!unpackPushMessage(data, &transaction, &uuid, &dict)) { + qCWarning(l) << "Failed to parse APP_MSG PUSH"; + watch->sendMessage(WatchConnector::watchAPPLICATION_MESSAGE, + buildNackMessage(transaction)); + return; + } + + qCDebug(l) << "Received appmsg PUSH from" << uuid << "with" << dict; + + QVariantMap msg = mapAppKeys(uuid, dict); + qCDebug(l) << "Mapped dict" << msg; + + bool result; + + MessageHandlerFunc handler = _handlers.value(uuid); + if (handler) { + result = handler(msg); + } else { + // No handler? Let's just send an ACK. + result = false; + } + + if (result) { + qCDebug(l) << "ACKing transaction" << transaction; + watch->sendMessage(WatchConnector::watchAPPLICATION_MESSAGE, + buildAckMessage(transaction)); + } else { + qCDebug(l) << "NACKing transaction" << transaction; + watch->sendMessage(WatchConnector::watchAPPLICATION_MESSAGE, + buildNackMessage(transaction)); + } +} + +void AppMsgManager::handleAckMessage(const QByteArray &data, bool ack) +{ + if (data.size() < 2) { + qCWarning(l) << "invalid ack/nack message size"; + return; + } + + const quint8 type = data[0]; Q_UNUSED(type); + const quint8 recv_transaction = data[1]; + + Q_ASSERT(type == WatchConnector::appmsgACK || type == WatchConnector::appmsgNACK); + + if (_pending.empty()) { + qCWarning(l) << "received an ack/nack for transaction" << recv_transaction << "but no transaction is pending"; + return; + } + + PendingTransaction &trans = _pending.head(); + if (trans.transactionId != recv_transaction) { + qCWarning(l) << "received an ack/nack but for the wrong transaction"; + } + + qCDebug(l) << "Got " << (ack ? "ACK" : "NACK") << " to transaction" << trans.transactionId; + + _timeout->stop(); + + if (ack) { + if (trans.ackCallback) { + trans.ackCallback(); + } + } else { + if (trans.nackCallback) { + trans.nackCallback(); + } + } + + _pending.dequeue(); + + if (!_pending.empty()) { + transmitNextPendingTransaction(); + } +} + +void AppMsgManager::handleWatchConnectedChanged() +{ + // If the watch is disconnected, everything breaks loose + // TODO In the future we may want to avoid doing the following. + if (!watch->isConnected()) { + abortPendingTransactions(); + } +} + +void AppMsgManager::handleTimeout() +{ + // Abort the first transaction + Q_ASSERT(!_pending.empty()); + PendingTransaction trans = _pending.dequeue(); + + qCWarning(l) << "timeout on appmsg transaction" << trans.transactionId; + + if (trans.nackCallback) { + trans.nackCallback(); + } + + if (!_pending.empty()) { + transmitNextPendingTransaction(); + } +} + +void AppMsgManager::transmitNextPendingTransaction() +{ + Q_ASSERT(!_pending.empty()); + PendingTransaction &trans = _pending.head(); + + QByteArray msg = buildPushMessage(trans.transactionId, trans.uuid, trans.dict); + + watch->sendMessage(WatchConnector::watchAPPLICATION_MESSAGE, msg); + + _timeout->start(); +} + +void AppMsgManager::abortPendingTransactions() +{ + // Invoke all the NACK callbacks in the pending queue, then drop them. + Q_FOREACH(const PendingTransaction &trans, _pending) { + if (trans.nackCallback) { + trans.nackCallback(); + } + } + + _pending.clear(); +} diff --git a/daemon/appmsgmanager.h b/daemon/appmsgmanager.h new file mode 100644 index 0000000..0a3acba --- /dev/null +++ b/daemon/appmsgmanager.h @@ -0,0 +1,77 @@ +#ifndef APPMSGMANAGER_H +#define APPMSGMANAGER_H + +#include <functional> +#include <QUuid> +#include <QQueue> + +#include "watchconnector.h" +#include "appmanager.h" + +class AppMsgManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit AppMsgManager(AppManager *apps, WatchConnector *watch, QObject *parent); + + void send(const QUuid &uuid, const QVariantMap &data, + const std::function<void()> &ackCallback, + const std::function<void()> &nackCallback); + + typedef std::function<bool(const QVariantMap &)> MessageHandlerFunc; + void setMessageHandler(const QUuid &uuid, MessageHandlerFunc func); + void clearMessageHandler(const QUuid &uuid); + + uint lastTransactionId() const; + uint nextTransactionId() const; + +public slots: + void send(const QUuid &uuid, const QVariantMap &data); + void launchApp(const QUuid &uuid); + void closeApp(const QUuid &uuid); + +signals: + void appStarted(const QUuid &uuid); + void appStopped(const QUuid &uuid); + +private: + WatchConnector::Dict mapAppKeys(const QUuid &uuid, const QVariantMap &data); + QVariantMap mapAppKeys(const QUuid &uuid, const WatchConnector::Dict &dict); + + static bool unpackPushMessage(const QByteArray &msg, quint8 *transaction, QUuid *uuid, WatchConnector::Dict *dict); + + static QByteArray buildPushMessage(quint8 transaction, const QUuid &uuid, const WatchConnector::Dict &dict); + static QByteArray buildAckMessage(quint8 transaction); + static QByteArray buildNackMessage(quint8 transaction); + + void handleLauncherPushMessage(const QByteArray &data); + void handlePushMessage(const QByteArray &data); + void handleAckMessage(const QByteArray &data, bool ack); + + void transmitNextPendingTransaction(); + void abortPendingTransactions(); + +private slots: + void handleWatchConnectedChanged(); + void handleTimeout(); + +private: + AppManager *apps; + WatchConnector *watch; + QHash<QUuid, MessageHandlerFunc> _handlers; + quint8 _lastTransactionId; + + struct PendingTransaction { + quint8 transactionId; + QUuid uuid; + WatchConnector::Dict dict; + std::function<void()> ackCallback; + std::function<void()> nackCallback; + }; + QQueue<PendingTransaction> _pending; + QTimer *_timeout; +}; + +#endif // APPMSGMANAGER_H diff --git a/daemon/bankmanager.cpp b/daemon/bankmanager.cpp new file mode 100644 index 0000000..f0aa68b --- /dev/null +++ b/daemon/bankmanager.cpp @@ -0,0 +1,373 @@ +#include <QFile> +#include <QDir> +#include "unpacker.h" +#include "packer.h" +#include "bankmanager.h" + +#if 0 +// TODO -- This is how language files seems to be installed. +if (slot == -4) { + qCDebug(l) << "starting lang install"; + QFile *pbl = new QFile(QDir::home().absoluteFilePath("es.pbl")); + if (!pbl->open(QIODevice::ReadOnly)) { + qCWarning(l) << "Failed to open pbl"; + return false; + } + + upload->uploadFile("lang", pbl, [this]() { + qCDebug(l) << "success"; + }, [this](int code) { + qCWarning(l) << "Some error" << code; + }); + + return true; +} +#endif + +BankManager::BankManager(WatchConnector *watch, UploadManager *upload, AppManager *apps, QObject *parent) : + QObject(parent), l(metaObject()->className()), + watch(watch), upload(upload), apps(apps), _refresh(new QTimer(this)) +{ + connect(watch, &WatchConnector::connectedChanged, + this, &BankManager::handleWatchConnected); + + _refresh->setInterval(0); + _refresh->setSingleShot(true); + connect(_refresh, &QTimer::timeout, + this, &BankManager::refresh); +} + +int BankManager::numSlots() const +{ + return _slots.size(); +} + +bool BankManager::isUsed(int slot) const +{ + return _slots.at(slot).used; +} + +QUuid BankManager::appAt(int slot) const +{ + return _slots.at(slot).uuid; +} + +bool BankManager::uploadApp(const QUuid &uuid, int slot) +{ + AppInfo info = apps->info(uuid); + if (info.uuid() != uuid) { + qCWarning(l) << "uuid" << uuid << "is not installed"; + return false; + } + if (slot == -1) { + slot = findUnusedSlot(); + if (slot == -1) { + qCWarning(l) << "no free slots!"; + return false; + } + } + if (slot < 0 || slot > _slots.size()) { + qCWarning(l) << "invalid slot index"; + return false; + } + if (_slots[slot].used) { + qCWarning(l) << "slot in use"; + return false; + } + + QDir appDir(info.path()); + + qCDebug(l) << "about to install app from" << appDir.absolutePath() << "into slot" << slot; + + QFile *binaryFile = new QFile(appDir.absoluteFilePath("pebble-app.bin"), this); + if (!binaryFile->open(QIODevice::ReadOnly)) { + qCWarning(l) << "failed to open" << binaryFile->fileName() << ":" << binaryFile->errorString(); + delete binaryFile; + return false; + } + + qCDebug(l) << "binary file size is" << binaryFile->size(); + + QFile *resourceFile = 0; + if (appDir.exists("app_resources.pbpack")) { + resourceFile = new QFile(appDir.absoluteFilePath("app_resources.pbpack"), this); + if (!resourceFile->open(QIODevice::ReadOnly)) { + qCWarning(l) << "failed to open" << resourceFile->fileName() << ":" << resourceFile->errorString(); + delete resourceFile; + return false; + } + } + + // Mark the slot as used, but without any app, just in case. + _slots[slot].used = true; + _slots[slot].name.clear(); + _slots[slot].uuid = QUuid(); + + upload->uploadAppBinary(slot, binaryFile, + [this, binaryFile, resourceFile, slot]() { + qCDebug(l) << "app binary upload succesful"; + delete binaryFile; + + // Proceed to upload the resource file + if (resourceFile) { + upload->uploadAppResources(slot, resourceFile, + [this, resourceFile, slot]() { + qCDebug(l) << "app resources upload succesful"; + delete resourceFile; + + // Upload succesful + // Tell the watch to reload the slot + refreshWatchApp(slot, [this]() { + qCDebug(l) << "app refresh succesful"; + _refresh->start(); + }, [this](int code) { + qCWarning(l) << "app refresh failed" << code; + _refresh->start(); + }); + }, [this, resourceFile](int code) { + qCWarning(l) << "app resources upload failed" << code; + delete resourceFile; + + _refresh->start(); + }); + + } else { + // No resource file + // Tell the watch to reload the slot + refreshWatchApp(slot, [this]() { + qCDebug(l) << "app refresh succesful"; + _refresh->start(); + }, [this](int code) { + qCWarning(l) << "app refresh failed" << code; + _refresh->start(); + }); + } + }, [this, binaryFile, resourceFile](int code) { + qCWarning(l) << "app binary upload failed" << code; + delete binaryFile; + delete resourceFile; + + _refresh->start(); + }); + + return true; +} + +bool BankManager::unloadApp(int slot) +{ + if (slot < 0 || slot > _slots.size()) { + qCWarning(l) << "invalid slot index"; + return false; + } + if (!_slots[slot].used) { + qCWarning(l) << "slot is empty"; + return false; + } + + qCDebug(l) << "going to unload app" << _slots[slot].name << "in slot" << slot; + + int installId = _slots[slot].id; + + QByteArray msg; + msg.reserve(1 + 2 * sizeof(quint32)); + Packer p(&msg); + p.write<quint8>(WatchConnector::appmgrREMOVE_APP); + p.write<quint32>(installId); + p.write<quint32>(slot); + + watch->sendMessage(WatchConnector::watchAPP_MANAGER, msg, + [this](const QByteArray &data) { + Unpacker u(data); + if (u.read<quint8>() != WatchConnector::appmgrREMOVE_APP) { + return false; + } + + uint result = u.read<quint32>(); + switch (result) { + case Success: /* Success */ + qCDebug(l) << "sucessfully unloaded app"; + break; + default: + qCWarning(l) << "could not unload app. result code:" << result; + break; + } + + _refresh->start(); + + return true; + }); + + return true; // Operation in progress +} + +void BankManager::refresh() +{ + qCDebug(l) << "refreshing bank status"; + + watch->sendMessage(WatchConnector::watchAPP_MANAGER, + QByteArray(1, WatchConnector::appmgrGET_APPBANK_STATUS), + [this](const QByteArray &data) { + if (data.at(0) != WatchConnector::appmgrGET_APPBANK_STATUS) { + return false; + } + + if (data.size() < 9) { + qCWarning(l) << "invalid getAppbankStatus response"; + return true; + } + + Unpacker u(data); + + u.skip(sizeof(quint8)); + + unsigned int num_banks = u.read<quint32>(); + unsigned int apps_installed = u.read<quint32>(); + + qCDebug(l) << "Bank status:" << apps_installed << "/" << num_banks; + + _slots.resize(num_banks); + for (unsigned int i = 0; i < num_banks; i++) { + _slots[i].used = false; + _slots[i].id = 0; + _slots[i].name.clear(); + _slots[i].company.clear(); + _slots[i].flags = 0; + _slots[i].version = 0; + _slots[i].uuid = QUuid(); + } + + for (unsigned int i = 0; i < apps_installed; i++) { + unsigned int id = u.read<quint32>(); + int index = u.read<quint32>(); + QString name = u.readFixedString(32); + QString company = u.readFixedString(32); + unsigned int flags = u.read<quint32>(); + unsigned short version = u.read<quint16>(); + + if (index < 0 || index >= _slots.size()) { + qCWarning(l) << "Invalid slot index" << index; + continue; + } + + if (u.bad()) { + qCWarning(l) << "short read"; + return true; + } + + _slots[index].used = true; + _slots[index].id = id; + _slots[index].name = name; + _slots[index].company = company; + _slots[index].flags = flags; + _slots[index].version = version; + + AppInfo info = apps->info(name); + QUuid uuid = info.uuid(); + _slots[index].uuid = uuid; + + qCDebug(l) << index << id << name << company << flags << version << uuid; + } + + emit this->slotsChanged(); + + return true; + }); +} + +int BankManager::findUnusedSlot() const +{ + for (int i = 0; i < _slots.size(); ++i) { + if (!_slots[i].used) { + return i; + } + } + + return -1; +} + +void BankManager::refreshWatchApp(int slot, std::function<void ()> successCallback, std::function<void (int)> errorCallback) +{ + QByteArray msg; + Packer p(&msg); + p.write<quint8>(WatchConnector::appmgrREFRESH_APP); + p.write<quint32>(slot); + + watch->sendMessage(WatchConnector::watchAPP_MANAGER, msg, + [this, successCallback, errorCallback](const QByteArray &data) { + Unpacker u(data); + int type = u.read<quint8>(); + // For some reason, the watch might sometimes reply an "app installed" message + // with a "app removed" confirmation message + // Every other implementation seems to ignore this fact, so I guess it's not important. + if (type != WatchConnector::appmgrREFRESH_APP && type != WatchConnector::appmgrREMOVE_APP) { + return false; + } + int code = u.read<quint32>(); + if (code == Success) { + if (successCallback) { + successCallback(); + } + } else { + if (errorCallback) { + errorCallback(code); + } + } + + return true; + }); +} + +void BankManager::handleWatchConnected() +{ + if (watch->isConnected()) { + _refresh->start(); + } +} + +#if 0 +void BankManager::getAppbankUuids(const function<void(const QList<QUuid> &)>& callback) +{ + watch->sendMessage(WatchConnector::watchAPP_MANAGER, + QByteArray(1, WatchConnector::appmgrGET_APPBANK_UUIDS), + [this, callback](const QByteArray &data) { + if (data.at(0) != WatchConnector::appmgrGET_APPBANK_UUIDS) { + return false; + } + qCDebug(l) << "getAppbankUuids response" << data.toHex(); + + if (data.size() < 5) { + qCWarning(l) << "invalid getAppbankUuids response"; + return true; + } + + Unpacker u(data); + + u.skip(sizeof(quint8)); + + unsigned int apps_installed = u.read<quint32>(); + + qCDebug(l) << apps_installed; + + QList<QUuid> uuids; + + for (unsigned int i = 0; i < apps_installed; i++) { + QUuid uuid = u.readUuid(); + + qCDebug(l) << uuid.toString(); + + if (u.bad()) { + qCWarning(l) << "short read"; + return true; + } + + uuids.push_back(uuid); + } + + qCDebug(l) << "finished"; + + callback(uuids); + + return true; + }); +} +#endif diff --git a/daemon/bankmanager.h b/daemon/bankmanager.h new file mode 100644 index 0000000..7532812 --- /dev/null +++ b/daemon/bankmanager.h @@ -0,0 +1,63 @@ +#ifndef BANKMANAGER_H +#define BANKMANAGER_H + +#include "watchconnector.h" +#include "uploadmanager.h" +#include "appmanager.h" + +class BankManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit BankManager(WatchConnector *watch, UploadManager *upload, AppManager *apps, QObject *parent = 0); + + int numSlots() const; + + bool isUsed(int slot) const; + QUuid appAt(int slot) const; + +signals: + void slotsChanged(); + +public slots: + bool uploadApp(const QUuid &uuid, int slot = -1); + bool unloadApp(int slot); + + void refresh(); + +private: + int findUnusedSlot() const; + void refreshWatchApp(int slot, std::function<void()> successCallback, std::function<void(int)> errorCallback); + +private slots: + void handleWatchConnected(); + +private: + WatchConnector *watch; + UploadManager *upload; + AppManager *apps; + + enum ResultCodes { + Success = 1, + BankInUse = 2, + InvalidCommand = 3, + GeneralFailure = 4 + }; + + struct SlotInfo { + bool used; + quint32 id; + QString name; + QString company; + quint32 flags; + quint16 version; + QUuid uuid; + }; + + QVector<SlotInfo> _slots; + QTimer *_refresh; +}; + +#endif // BANKMANAGER_H diff --git a/daemon/daemon.cpp b/daemon/daemon.cpp index e4306da..178f04d 100644 --- a/daemon/daemon.cpp +++ b/daemon/daemon.cpp @@ -51,6 +51,7 @@ void signalhandler(int sig) int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); + app.setApplicationName("pebble"); // Use the same appname as the UI. QStringList filterRules; @@ -64,16 +65,11 @@ int main(int argc, char *argv[]) qCDebug(l) << argv[0] << APP_VERSION; Settings settings; - watch::WatchConnector watch; - DBusConnector dbus; - VoiceCallManager voice(&settings); - NotificationManager notifications(&settings); - Manager manager(&watch, &dbus, &voice, ¬ifications, &settings); + Manager manager(&settings); + Q_UNUSED(manager); signal(SIGINT, signalhandler); signal(SIGTERM, signalhandler); - QObject::connect(&app, SIGNAL(aboutToQuit()), &watch, SLOT(endPhoneCall())); - QObject::connect(&app, SIGNAL(aboutToQuit()), &watch, SLOT(disconnect())); return app.exec(); } diff --git a/daemon/daemon.pro b/daemon/daemon.pro index 85705aa..1c287d0 100644 --- a/daemon/daemon.pro +++ b/daemon/daemon.pro @@ -2,13 +2,10 @@ TARGET = pebbled CONFIG += console CONFIG += link_pkgconfig -QT -= gui -QT += core bluetooth dbus contacts -PKGCONFIG += mlite5 -QMAKE_CXXFLAGS += -std=c++0x - -LIBS += -licuuc -licui18n +QT += core gui qml bluetooth dbus contacts positioning +PKGCONFIG += mlite5 icu-i18n +CONFIG += c++11 DEFINES += APP_VERSION=\\\"$$VERSION\\\" @@ -20,8 +17,18 @@ SOURCES += \ notificationmanager.cpp \ watchconnector.cpp \ dbusconnector.cpp \ - dbusadaptor.cpp \ - watchcommands.cpp + appmanager.cpp \ + musicmanager.cpp \ + datalogmanager.cpp \ + unpacker.cpp \ + appmsgmanager.cpp \ + jskitmanager.cpp \ + appinfo.cpp \ + jskitobjects.cpp \ + packer.cpp \ + bankmanager.cpp \ + uploadmanager.cpp \ + stm32crc.cpp HEADERS += \ manager.h \ @@ -30,19 +37,34 @@ HEADERS += \ notificationmanager.h \ watchconnector.h \ dbusconnector.h \ - dbusadaptor.h \ - watchcommands.h \ - settings.h + settings.h \ + appmanager.h \ + musicmanager.h \ + unpacker.h \ + datalogmanager.h \ + appmsgmanager.h \ + jskitmanager.h \ + appinfo.h \ + jskitobjects.h \ + packer.h \ + bankmanager.h \ + uploadmanager.h \ + stm32crc.h + +DBUS_ADAPTORS += ../org.pebbled.Watch.xml -OTHER_FILES += \ - org.pebbled.xml +OTHER_FILES += $$DBUS_ADAPTORS \ + js/typedarray.js -INSTALLS += target pebbled +INSTALLS += target systemd js target.path = /usr/bin -pebbled.files = $${TARGET}.service -pebbled.path = /usr/lib/systemd/user +systemd.files = $${TARGET}.service +systemd.path = /usr/lib/systemd/user + +js.files = js/* +js.path = /usr/share/pebble/js # unnecesary includes, just so QtCreator could find headers... :-( INCLUDEPATH += $$[QT_HOST_PREFIX]/include/mlite5 diff --git a/daemon/datalogmanager.cpp b/daemon/datalogmanager.cpp new file mode 100644 index 0000000..c3562ef --- /dev/null +++ b/daemon/datalogmanager.cpp @@ -0,0 +1,42 @@ +#include "datalogmanager.h" +#include "unpacker.h" + +DataLogManager::DataLogManager(WatchConnector *watch, QObject *parent) : + QObject(parent), l(metaObject()->className()), watch(watch) +{ + watch->setEndpointHandler(WatchConnector::watchDATA_LOGGING, [this](const QByteArray& data) { + if (data.size() < 2) { + qCWarning(l) << "small data_logging packet"; + return false; + } + + const char command = data[0]; + const int session = data[1]; + + switch (command) { + case WatchConnector::datalogOPEN: + qCDebug(l) << "open datalog session" << session; + return true; + case WatchConnector::datalogCLOSE: + qCDebug(l) << "close datalog session" << session; + return true; + case WatchConnector::datalogTIMEOUT: + qCDebug(l) << "timeout datalog session" << session; + return true; + case WatchConnector::datalogDATA: + handleDataCommand(session, data.mid(2)); + return true; + default: + return false; + } + }); +} + +void DataLogManager::handleDataCommand(int session, const QByteArray &data) +{ + Unpacker u(data); + + // TODO Seemingly related to analytics, so not important. + + qCDebug(l) << "got datalog data" << session << data.size(); +} diff --git a/daemon/datalogmanager.h b/daemon/datalogmanager.h new file mode 100644 index 0000000..b36875a --- /dev/null +++ b/daemon/datalogmanager.h @@ -0,0 +1,25 @@ +#ifndef DATALOGMANAGER_H +#define DATALOGMANAGER_H + +#include "watchconnector.h" + +class DataLogManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit DataLogManager(WatchConnector *watch, QObject *parent = 0); + +signals: + +public slots: + +private: + void handleDataCommand(int session, const QByteArray &data); + +private: + WatchConnector *watch; +}; + +#endif // DATALOGMANAGER_H diff --git a/daemon/dbusadaptor.cpp b/daemon/dbusadaptor.cpp deleted file mode 100644 index 3332551..0000000 --- a/daemon/dbusadaptor.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * This file was generated by qdbusxml2cpp version 0.8 - * Command line was: qdbusxml2cpp -a dbusadaptor org.pebbled.xml - * - * qdbusxml2cpp is Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). - * - * This is an auto-generated file. - * Do not edit! All changes made to it will be lost. - */ - -#include "dbusadaptor.h" -#include <QtCore/QMetaObject> -#include <QtCore/QByteArray> -#include <QtCore/QList> -#include <QtCore/QMap> -#include <QtCore/QString> -#include <QtCore/QStringList> -#include <QtCore/QVariant> - -/* - * Implementation of adaptor class PebbledAdaptor - */ - -PebbledAdaptor::PebbledAdaptor(QObject *parent) - : QDBusAbstractAdaptor(parent) -{ - // constructor - setAutoRelaySignals(true); -} - -PebbledAdaptor::~PebbledAdaptor() -{ - // destructor -} - -QString PebbledAdaptor::address() const -{ - // get the value of property address - return qvariant_cast< QString >(parent()->property("address")); -} - -bool PebbledAdaptor::connected() const -{ - // get the value of property connected - return qvariant_cast< bool >(parent()->property("connected")); -} - -QString PebbledAdaptor::name() const -{ - // get the value of property name - return qvariant_cast< QString >(parent()->property("name")); -} - -QVariantMap PebbledAdaptor::pebble() const -{ - // get the value of property pebble - return qvariant_cast< QVariantMap >(parent()->property("pebble")); -} - -void PebbledAdaptor::disconnect() -{ - // handle method call org.pebbled.disconnect - QMetaObject::invokeMethod(parent(), "disconnect"); -} - -void PebbledAdaptor::ping(int val) -{ - // handle method call org.pebbled.ping - QMetaObject::invokeMethod(parent(), "ping", Q_ARG(int, val)); -} - -void PebbledAdaptor::time() -{ - // handle method call org.pebbled.time - QMetaObject::invokeMethod(parent(), "time"); -} - - -void PebbledAdaptor::reconnect() -{ - // handle method call org.pebbled.reconnect - QMetaObject::invokeMethod(parent(), "reconnect"); -} - diff --git a/daemon/dbusadaptor.h b/daemon/dbusadaptor.h deleted file mode 100644 index 715a41b..0000000 --- a/daemon/dbusadaptor.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file was generated by qdbusxml2cpp version 0.8 - * Command line was: qdbusxml2cpp -a dbusadaptor org.pebbled.xml - * - * qdbusxml2cpp is Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). - * - * This is an auto-generated file. - * This file may have been hand-edited. Look for HAND-EDIT comments - * before re-generating it. - */ - -#ifndef DBUSADAPTOR_H_1404986135 -#define DBUSADAPTOR_H_1404986135 - -#include <QtCore/QObject> -#include <QtDBus/QtDBus> -QT_BEGIN_NAMESPACE -class QByteArray; -template<class T> class QList; -template<class Key, class Value> class QMap; -class QString; -class QStringList; -class QVariant; -QT_END_NAMESPACE - -/* - * Adaptor class for interface org.pebbled - */ -class PebbledAdaptor: public QDBusAbstractAdaptor -{ - Q_OBJECT - Q_CLASSINFO("D-Bus Interface", "org.pebbled") - Q_CLASSINFO("D-Bus Introspection", "" -" <interface name=\"org.pebbled\">\n" -" <property access=\"read\" type=\"a{sv}\" name=\"pebble\">\n" -" <annotation value=\"QVariantMap\" name=\"org.qtproject.QtDBus.QtTypeName\"/>\n" -" </property>\n" -" <property access=\"read\" type=\"s\" name=\"name\"/>\n" -" <property access=\"read\" type=\"s\" name=\"address\"/>\n" -" <property access=\"read\" type=\"b\" name=\"connected\"/>\n" -" <signal name=\"pebbleChanged\"/>\n" -" <signal name=\"connectedChanged\"/>\n" -" <method name=\"ping\">\n" -" <arg direction=\"in\" type=\"i\" name=\"val\"/>\n" -" </method>\n" -" <method name=\"time\"/>\n" -" <method name=\"disconnect\"/>\n" -" <method name=\"reconnect\"/>\n" -" </interface>\n" - "") -public: - PebbledAdaptor(QObject *parent); - virtual ~PebbledAdaptor(); - -public: // PROPERTIES - Q_PROPERTY(QString address READ address) - QString address() const; - - Q_PROPERTY(bool connected READ connected) - bool connected() const; - - Q_PROPERTY(QString name READ name) - QString name() const; - - Q_PROPERTY(QVariantMap pebble READ pebble) - QVariantMap pebble() const; - -public Q_SLOTS: // METHODS - void disconnect(); - void ping(int val); - void time(); - void reconnect(); -Q_SIGNALS: // SIGNALS - void connectedChanged(); - void pebbleChanged(); -}; - -#endif diff --git a/daemon/dbusconnector.cpp b/daemon/dbusconnector.cpp index 4827b1f..197a12f 100644 --- a/daemon/dbusconnector.cpp +++ b/daemon/dbusconnector.cpp @@ -6,7 +6,6 @@ #include <QDBusReply> #include <QDBusArgument> #include <QDBusObjectPath> -#include <QDBusConnectionInterface> //dbus-send --system --dest=org.bluez --print-reply / org.bluez.Manager.ListAdapters //dbus-send --system --dest=org.bluez --print-reply $path org.bluez.Adapter.GetProperties @@ -15,19 +14,7 @@ DBusConnector::DBusConnector(QObject *parent) : QObject(parent), l(metaObject()->className()) -{ - QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); - - QDBusReply<QStringList> serviceNames = interface->registeredServiceNames(); - if (serviceNames.isValid()) { - dbusServices = serviceNames.value(); - } - else { - qCCritical(l) << serviceNames.error().message(); - } - connect(interface, SIGNAL(serviceRegistered(const QString &)), SLOT(onServiceRegistered(const QString &))); - connect(interface, SIGNAL(serviceUnregistered(const QString &)), SLOT(onServiceUnregistered(const QString &))); -} +{} bool DBusConnector::findPebble() { @@ -82,15 +69,3 @@ bool DBusConnector::findPebble() return false; } - -void DBusConnector::onServiceRegistered(const QString &name) -{ - qCDebug(l) << "DBus service online:" << name; - if (!dbusServices.contains(name)) dbusServices.append(name); -} - -void DBusConnector::onServiceUnregistered(const QString &name) -{ - qCDebug(l) << "DBus service offline:" << name; - if (dbusServices.contains(name)) dbusServices.removeAll(name); -} diff --git a/daemon/dbusconnector.h b/daemon/dbusconnector.h index e44ff3f..6b48f99 100644 --- a/daemon/dbusconnector.h +++ b/daemon/dbusconnector.h @@ -6,34 +6,26 @@ #include <QVariantMap> #include <QLoggingCategory> +// TODO Remove this. + class DBusConnector : public QObject { Q_OBJECT QLoggingCategory l; Q_PROPERTY(QVariantMap pebble READ pebble NOTIFY pebbleChanged) - Q_PROPERTY(QStringList services READ services NOTIFY servicesChanged) - QVariantMap pebbleProps; - QStringList dbusServices; public: explicit DBusConnector(QObject *parent = 0); - QVariantMap pebble() { return pebbleProps; } - QStringList services() { return dbusServices; } + QVariantMap pebble() const { return pebbleProps; } signals: void pebbleChanged(); - void servicesChanged(); public slots: bool findPebble(); - -protected slots: - void onServiceRegistered(const QString &); - void onServiceUnregistered(const QString &); - }; #endif // DBUSCONNECTOR_H diff --git a/daemon/js/typedarray.js b/daemon/js/typedarray.js new file mode 100644 index 0000000..eec78a2 --- /dev/null +++ b/daemon/js/typedarray.js @@ -0,0 +1,1030 @@ +/* + Copyright (c) 2010, Linden Research, Inc. + Copyright (c) 2014, Joshua Bell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + $/LicenseInfo$ + */ + +// Original can be found at: +// https://bitbucket.org/lindenlab/llsd +// Modifications by Joshua Bell inexorabletash@gmail.com +// https://github.com/inexorabletash/polyfill + +// ES3/ES5 implementation of the Krhonos Typed Array Specification +// Ref: http://www.khronos.org/registry/typedarray/specs/latest/ +// Date: 2011-02-01 +// +// Variations: +// * Allows typed_array.get/set() as alias for subscripts (typed_array[]) +// * Gradually migrating structure from Khronos spec to ES6 spec +(function(global) { + 'use strict'; + var undefined = (void 0); // Paranoia + + // Beyond this value, index getters/setters (i.e. array[0], array[1]) are so slow to + // create, and consume so much memory, that the browser appears frozen. + var MAX_ARRAY_LENGTH = 1e5; + + // Approximations of internal ECMAScript conversion functions + function Type(v) { + switch(typeof v) { + case 'undefined': return 'undefined'; + case 'boolean': return 'boolean'; + case 'number': return 'number'; + case 'string': return 'string'; + default: return v === null ? 'null' : 'object'; + } + } + + // Class returns internal [[Class]] property, used to avoid cross-frame instanceof issues: + function Class(v) { return Object.prototype.toString.call(v).replace(/^\[object *|\]$/g, ''); } + function IsCallable(o) { return typeof o === 'function'; } + function ToObject(v) { + if (v === null || v === undefined) throw TypeError(); + return Object(v); + } + function ToInt32(v) { return v >> 0; } + function ToUint32(v) { return v >>> 0; } + + // Snapshot intrinsics + var LN2 = Math.LN2, + abs = Math.abs, + floor = Math.floor, + log = Math.log, + max = Math.max, + min = Math.min, + pow = Math.pow, + round = Math.round; + + // emulate ES5 getter/setter API using legacy APIs + // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx + // (second clause tests for Object.defineProperty() in IE<9 that only supports extending DOM prototypes, but + // note that IE<9 does not support __defineGetter__ or __defineSetter__ so it just renders the method harmless) + + (function() { + var orig = Object.defineProperty; + var dom_only = !(function(){try{return Object.defineProperty({},'x',{});}catch(_){return false;}}()); + + if (!orig || dom_only) { + Object.defineProperty = function (o, prop, desc) { + // In IE8 try built-in implementation for defining properties on DOM prototypes. + if (orig) + try { return orig(o, prop, desc); } catch (_) {} + if (o !== Object(o)) + throw TypeError('Object.defineProperty called on non-object'); + if (Object.prototype.__defineGetter__ && ('get' in desc)) + Object.prototype.__defineGetter__.call(o, prop, desc.get); + if (Object.prototype.__defineSetter__ && ('set' in desc)) + Object.prototype.__defineSetter__.call(o, prop, desc.set); + if ('value' in desc) + o[prop] = desc.value; + return o; + }; + } + }()); + + // ES5: Make obj[index] an alias for obj._getter(index)/obj._setter(index, value) + // for index in 0 ... obj.length + function makeArrayAccessors(obj) { + if (obj.length > MAX_ARRAY_LENGTH) throw RangeError('Array too large for polyfill'); + + function makeArrayAccessor(index) { + Object.defineProperty(obj, index, { + 'get': function() { return obj._getter(index); }, + 'set': function(v) { obj._setter(index, v); }, + enumerable: true, + configurable: false + }); + } + + var i; + for (i = 0; i < obj.length; i += 1) { + makeArrayAccessor(i); + } + } + + // Internal conversion functions: + // pack<Type>() - take a number (interpreted as Type), output a byte array + // unpack<Type>() - take a byte array, output a Type-like number + + function as_signed(value, bits) { var s = 32 - bits; return (value << s) >> s; } + function as_unsigned(value, bits) { var s = 32 - bits; return (value << s) >>> s; } + + function packI8(n) { return [n & 0xff]; } + function unpackI8(bytes) { return as_signed(bytes[0], 8); } + + function packU8(n) { return [n & 0xff]; } + function unpackU8(bytes) { return as_unsigned(bytes[0], 8); } + + function packU8Clamped(n) { n = round(Number(n)); return [n < 0 ? 0 : n > 0xff ? 0xff : n & 0xff]; } + + function packI16(n) { return [(n >> 8) & 0xff, n & 0xff]; } + function unpackI16(bytes) { return as_signed(bytes[0] << 8 | bytes[1], 16); } + + function packU16(n) { return [(n >> 8) & 0xff, n & 0xff]; } + function unpackU16(bytes) { return as_unsigned(bytes[0] << 8 | bytes[1], 16); } + + function packI32(n) { return [(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]; } + function unpackI32(bytes) { return as_signed(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3], 32); } + + function packU32(n) { return [(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]; } + function unpackU32(bytes) { return as_unsigned(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3], 32); } + + function packIEEE754(v, ebits, fbits) { + + var bias = (1 << (ebits - 1)) - 1, + s, e, f, ln, + i, bits, str, bytes; + + function roundToEven(n) { + var w = floor(n), f = n - w; + if (f < 0.5) + return w; + if (f > 0.5) + return w + 1; + return w % 2 ? w + 1 : w; + } + + // Compute sign, exponent, fraction + if (v !== v) { + // NaN + // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping + e = (1 << ebits) - 1; f = pow(2, fbits - 1); s = 0; + } else if (v === Infinity || v === -Infinity) { + e = (1 << ebits) - 1; f = 0; s = (v < 0) ? 1 : 0; + } else if (v === 0) { + e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0; + } else { + s = v < 0; + v = abs(v); + + if (v >= pow(2, 1 - bias)) { + e = min(floor(log(v) / LN2), 1023); + f = roundToEven(v / pow(2, e) * pow(2, fbits)); + if (f / pow(2, fbits) >= 2) { + e = e + 1; + f = 1; + } + if (e > bias) { + // Overflow + e = (1 << ebits) - 1; + f = 0; + } else { + // Normalized + e = e + bias; + f = f - pow(2, fbits); + } + } else { + // Denormalized + e = 0; + f = roundToEven(v / pow(2, 1 - bias - fbits)); + } + } + + // Pack sign, exponent, fraction + bits = []; + for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = floor(f / 2); } + for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = floor(e / 2); } + bits.push(s ? 1 : 0); + bits.reverse(); + str = bits.join(''); + + // Bits to bytes + bytes = []; + while (str.length) { + bytes.push(parseInt(str.substring(0, 8), 2)); + str = str.substring(8); + } + return bytes; + } + + function unpackIEEE754(bytes, ebits, fbits) { + // Bytes to bits + var bits = [], i, j, b, str, + bias, s, e, f; + + for (i = bytes.length; i; i -= 1) { + b = bytes[i - 1]; + for (j = 8; j; j -= 1) { + bits.push(b % 2 ? 1 : 0); b = b >> 1; + } + } + bits.reverse(); + str = bits.join(''); + + // Unpack sign, exponent, fraction + bias = (1 << (ebits - 1)) - 1; + s = parseInt(str.substring(0, 1), 2) ? -1 : 1; + e = parseInt(str.substring(1, 1 + ebits), 2); + f = parseInt(str.substring(1 + ebits), 2); + + // Produce number + if (e === (1 << ebits) - 1) { + return f !== 0 ? NaN : s * Infinity; + } else if (e > 0) { + // Normalized + return s * pow(2, e - bias) * (1 + f / pow(2, fbits)); + } else if (f !== 0) { + // Denormalized + return s * pow(2, -(bias - 1)) * (f / pow(2, fbits)); + } else { + return s < 0 ? -0 : 0; + } + } + + function unpackF64(b) { return unpackIEEE754(b, 11, 52); } + function packF64(v) { return packIEEE754(v, 11, 52); } + function unpackF32(b) { return unpackIEEE754(b, 8, 23); } + function packF32(v) { return packIEEE754(v, 8, 23); } + + // + // 3 The ArrayBuffer Type + // + + (function() { + + function ArrayBuffer(length) { + length = ToInt32(length); + if (length < 0) throw RangeError('ArrayBuffer size is not a small enough positive integer.'); + Object.defineProperty(this, 'byteLength', {value: length}); + Object.defineProperty(this, '_bytes', {value: Array(length)}); + + for (var i = 0; i < length; i += 1) + this._bytes[i] = 0; + } + + global.ArrayBuffer = global.ArrayBuffer || ArrayBuffer; + + // + // 5 The Typed Array View Types + // + + function $TypedArray$() { + + // %TypedArray% ( length ) + if (!arguments.length || typeof arguments[0] !== 'object') { + return (function(length) { + length = ToInt32(length); + if (length < 0) throw RangeError('length is not a small enough positive integer.'); + Object.defineProperty(this, 'length', {value: length}); + Object.defineProperty(this, 'byteLength', {value: length * this.BYTES_PER_ELEMENT}); + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(this.byteLength)}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + + }).apply(this, arguments); + } + + // %TypedArray% ( typedArray ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + arguments[0] instanceof $TypedArray$) { + return (function(typedArray){ + if (this.constructor !== typedArray.constructor) throw TypeError(); + + var byteLength = typedArray.length * this.BYTES_PER_ELEMENT; + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + Object.defineProperty(this, 'length', {value: typedArray.length}); + + for (var i = 0; i < this.length; i += 1) + this._setter(i, typedArray._getter(i)); + + }).apply(this, arguments); + } + + // %TypedArray% ( array ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + !(arguments[0] instanceof $TypedArray$) && + !(arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { + return (function(array) { + + var byteLength = array.length * this.BYTES_PER_ELEMENT; + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + Object.defineProperty(this, 'length', {value: array.length}); + + for (var i = 0; i < this.length; i += 1) { + var s = array[i]; + this._setter(i, Number(s)); + } + }).apply(this, arguments); + } + + // %TypedArray% ( buffer, byteOffset=0, length=undefined ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + (arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { + return (function(buffer, byteOffset, length) { + + byteOffset = ToUint32(byteOffset); + if (byteOffset > buffer.byteLength) + throw RangeError('byteOffset out of range'); + + // The given byteOffset must be a multiple of the element + // size of the specific type, otherwise an exception is raised. + if (byteOffset % this.BYTES_PER_ELEMENT) + throw RangeError('buffer length minus the byteOffset is not a multiple of the element size.'); + + if (length === undefined) { + var byteLength = buffer.byteLength - byteOffset; + if (byteLength % this.BYTES_PER_ELEMENT) + throw RangeError('length of buffer minus byteOffset not a multiple of the element size'); + length = byteLength / this.BYTES_PER_ELEMENT; + + } else { + length = ToUint32(length); + byteLength = length * this.BYTES_PER_ELEMENT; + } + + if ((byteOffset + byteLength) > buffer.byteLength) + throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); + + Object.defineProperty(this, 'buffer', {value: buffer}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: byteOffset}); + Object.defineProperty(this, 'length', {value: length}); + + }).apply(this, arguments); + } + + // %TypedArray% ( all other argument combinations ) + throw TypeError(); + } + + // Properties of the %TypedArray Instrinsic Object + + // %TypedArray%.from ( source , mapfn=undefined, thisArg=undefined ) + Object.defineProperty($TypedArray$, 'from', {value: function(iterable) { + return new this(iterable); + }}); + + // %TypedArray%.of ( ...items ) + Object.defineProperty($TypedArray$, 'of', {value: function(/*...items*/) { + return new this(arguments); + }}); + + // %TypedArray%.prototype + var $TypedArrayPrototype$ = {}; + $TypedArray$.prototype = $TypedArrayPrototype$; + + // WebIDL: getter type (unsigned long index); + Object.defineProperty($TypedArray$.prototype, '_getter', {value: function(index) { + if (arguments.length < 1) throw SyntaxError('Not enough arguments'); + + index = ToUint32(index); + if (index >= this.length) + return undefined; + + var bytes = [], i, o; + for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; + i < this.BYTES_PER_ELEMENT; + i += 1, o += 1) { + bytes.push(this.buffer._bytes[o]); + } + return this._unpack(bytes); + }}); + + // NONSTANDARD: convenience alias for getter: type get(unsigned long index); + Object.defineProperty($TypedArray$.prototype, 'get', {value: $TypedArray$.prototype._getter}); + + // WebIDL: setter void (unsigned long index, type value); + Object.defineProperty($TypedArray$.prototype, '_setter', {value: function(index, value) { + if (arguments.length < 2) throw SyntaxError('Not enough arguments'); + + index = ToUint32(index); + if (index >= this.length) + return; + + var bytes = this._pack(value), i, o; + for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; + i < this.BYTES_PER_ELEMENT; + i += 1, o += 1) { + this.buffer._bytes[o] = bytes[i]; + } + }}); + + // get %TypedArray%.prototype.buffer + // get %TypedArray%.prototype.byteLength + // get %TypedArray%.prototype.byteOffset + // -- applied directly to the object in the constructor + + // %TypedArray%.prototype.constructor + Object.defineProperty($TypedArray$.prototype, 'constructor', {value: $TypedArray$}); + + // %TypedArray%.prototype.copyWithin (target, start, end = this.length ) + Object.defineProperty($TypedArray$.prototype, 'copyWithin', {value: function(target, start) { + var end = arguments[2]; + + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + len = max(len, 0); + var relativeTarget = ToInt32(target); + var to; + if (relativeTarget < 0) + to = max(len + relativeTarget, 0); + else + to = min(relativeTarget, len); + var relativeStart = ToInt32(start); + var from; + if (relativeStart < 0) + from = max(len + relativeStart, 0); + else + from = min(relativeStart, len); + var relativeEnd; + if (end === undefined) + relativeEnd = len; + else + relativeEnd = ToInt32(end); + var final; + if (relativeEnd < 0) + final = max(len + relativeEnd, 0); + else + final = min(relativeEnd, len); + var count = min(final - from, len - to); + var direction; + if (from < to && to < from + count) { + direction = -1; + from = from + count - 1; + to = to + count - 1; + } else { + direction = 1; + } + while (count > 0) { + o._setter(to, o._getter(from)); + from = from + direction; + to = to + direction; + count = count - 1; + } + return o; + }}); + + // %TypedArray%.prototype.entries ( ) + // -- defined in es6.js to shim browsers w/ native TypedArrays + + // %TypedArray%.prototype.every ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'every', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisArg = arguments[1]; + for (var i = 0; i < len; i++) { + if (!callbackfn.call(thisArg, t._getter(i), i, t)) + return false; + } + return true; + }}); + + // %TypedArray%.prototype.fill (value, start = 0, end = this.length ) + Object.defineProperty($TypedArray$.prototype, 'fill', {value: function(value) { + var start = arguments[1], + end = arguments[2]; + + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + len = max(len, 0); + var relativeStart = ToInt32(start); + var k; + if (relativeStart < 0) + k = max((len + relativeStart), 0); + else + k = min(relativeStart, len); + var relativeEnd; + if (end === undefined) + relativeEnd = len; + else + relativeEnd = ToInt32(end); + var final; + if (relativeEnd < 0) + final = max((len + relativeEnd), 0); + else + final = min(relativeEnd, len); + while (k < final) { + o._setter(k, value); + k += 1; + } + return o; + }}); + + // %TypedArray%.prototype.filter ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'filter', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + var val = t._getter(i); // in case fun mutates this + if (callbackfn.call(thisp, val, i, t)) + res.push(val); + } + return new this.constructor(res); + }}); + + // %TypedArray%.prototype.find (predicate, thisArg = undefined) + Object.defineProperty($TypedArray$.prototype, 'find', {value: function(predicate) { + var o = ToObject(this); + var lenValue = o.length; + var len = ToUint32(lenValue); + if (!IsCallable(predicate)) throw TypeError(); + var t = arguments.length > 1 ? arguments[1] : undefined; + var k = 0; + while (k < len) { + var kValue = o._getter(k); + var testResult = predicate.call(t, kValue, k, o); + if (Boolean(testResult)) + return kValue; + ++k; + } + return undefined; + }}); + + // %TypedArray%.prototype.findIndex ( predicate, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'findIndex', {value: function(predicate) { + var o = ToObject(this); + var lenValue = o.length; + var len = ToUint32(lenValue); + if (!IsCallable(predicate)) throw TypeError(); + var t = arguments.length > 1 ? arguments[1] : undefined; + var k = 0; + while (k < len) { + var kValue = o._getter(k); + var testResult = predicate.call(t, kValue, k, o); + if (Boolean(testResult)) + return k; + ++k; + } + return -1; + }}); + + // %TypedArray%.prototype.forEach ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'forEach', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + callbackfn.call(thisp, t._getter(i), i, t); + }}); + + // %TypedArray%.prototype.indexOf (searchElement, fromIndex = 0 ) + Object.defineProperty($TypedArray$.prototype, 'indexOf', {value: function(searchElement) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (len === 0) return -1; + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * floor(abs(n)); + } + } + if (n >= len) return -1; + var k = n >= 0 ? n : max(len - abs(n), 0); + for (; k < len; k++) { + if (t._getter(k) === searchElement) { + return k; + } + } + return -1; + }}); + + // %TypedArray%.prototype.join ( separator ) + Object.defineProperty($TypedArray$.prototype, 'join', {value: function(separator) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var tmp = Array(len); + for (var i = 0; i < len; ++i) + tmp[i] = t._getter(i); + return tmp.join(separator === undefined ? ',' : separator); // Hack for IE7 + }}); + + // %TypedArray%.prototype.keys ( ) + // -- defined in es6.js to shim browsers w/ native TypedArrays + + // %TypedArray%.prototype.lastIndexOf ( searchElement, fromIndex = this.length-1 ) + Object.defineProperty($TypedArray$.prototype, 'lastIndexOf', {value: function(searchElement) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (len === 0) return -1; + var n = len; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n !== n) { + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * floor(abs(n)); + } + } + var k = n >= 0 ? min(n, len - 1) : len - abs(n); + for (; k >= 0; k--) { + if (t._getter(k) === searchElement) + return k; + } + return -1; + }}); + + // get %TypedArray%.prototype.length + // -- applied directly to the object in the constructor + + // %TypedArray%.prototype.map ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'map', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var res = []; res.length = len; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + res[i] = callbackfn.call(thisp, t._getter(i), i, t); + return new this.constructor(res); + }}); + + // %TypedArray%.prototype.reduce ( callbackfn [, initialValue] ) + Object.defineProperty($TypedArray$.prototype, 'reduce', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + // no value to return if no initial value and an empty array + if (len === 0 && arguments.length === 1) throw TypeError(); + var k = 0; + var accumulator; + if (arguments.length >= 2) { + accumulator = arguments[1]; + } else { + accumulator = t._getter(k++); + } + while (k < len) { + accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); + k++; + } + return accumulator; + }}); + + // %TypedArray%.prototype.reduceRight ( callbackfn [, initialValue] ) + Object.defineProperty($TypedArray$.prototype, 'reduceRight', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + // no value to return if no initial value, empty array + if (len === 0 && arguments.length === 1) throw TypeError(); + var k = len - 1; + var accumulator; + if (arguments.length >= 2) { + accumulator = arguments[1]; + } else { + accumulator = t._getter(k--); + } + while (k >= 0) { + accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); + k--; + } + return accumulator; + }}); + + // %TypedArray%.prototype.reverse ( ) + Object.defineProperty($TypedArray$.prototype, 'reverse', {value: function() { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var half = floor(len / 2); + for (var i = 0, j = len - 1; i < half; ++i, --j) { + var tmp = t._getter(i); + t._setter(i, t._getter(j)); + t._setter(j, tmp); + } + return t; + }}); + + // %TypedArray%.prototype.set(array, offset = 0 ) + // %TypedArray%.prototype.set(typedArray, offset = 0 ) + // WebIDL: void set(TypedArray array, optional unsigned long offset); + // WebIDL: void set(sequence<type> array, optional unsigned long offset); + Object.defineProperty($TypedArray$.prototype, 'set', {value: function(index, value) { + if (arguments.length < 1) throw SyntaxError('Not enough arguments'); + var array, sequence, offset, len, + i, s, d, + byteOffset, byteLength, tmp; + + if (typeof arguments[0] === 'object' && arguments[0].constructor === this.constructor) { + // void set(TypedArray array, optional unsigned long offset); + array = arguments[0]; + offset = ToUint32(arguments[1]); + + if (offset + array.length > this.length) { + throw RangeError('Offset plus length of array is out of range'); + } + + byteOffset = this.byteOffset + offset * this.BYTES_PER_ELEMENT; + byteLength = array.length * this.BYTES_PER_ELEMENT; + + if (array.buffer === this.buffer) { + tmp = []; + for (i = 0, s = array.byteOffset; i < byteLength; i += 1, s += 1) { + tmp[i] = array.buffer._bytes[s]; + } + for (i = 0, d = byteOffset; i < byteLength; i += 1, d += 1) { + this.buffer._bytes[d] = tmp[i]; + } + } else { + for (i = 0, s = array.byteOffset, d = byteOffset; + i < byteLength; i += 1, s += 1, d += 1) { + this.buffer._bytes[d] = array.buffer._bytes[s]; + } + } + } else if (typeof arguments[0] === 'object' && typeof arguments[0].length !== 'undefined') { + // void set(sequence<type> array, optional unsigned long offset); + sequence = arguments[0]; + len = ToUint32(sequence.length); + offset = ToUint32(arguments[1]); + + if (offset + len > this.length) { + throw RangeError('Offset plus length of array is out of range'); + } + + for (i = 0; i < len; i += 1) { + s = sequence[i]; + this._setter(offset + i, Number(s)); + } + } else { + throw TypeError('Unexpected argument type(s)'); + } + }}); + + // %TypedArray%.prototype.slice ( start, end ) + Object.defineProperty($TypedArray$.prototype, 'slice', {value: function(start, end) { + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + var relativeStart = ToInt32(start); + var k = (relativeStart < 0) ? max(len + relativeStart, 0) : min(relativeStart, len); + var relativeEnd = (end === undefined) ? len : ToInt32(end); + var final = (relativeEnd < 0) ? max(len + relativeEnd, 0) : min(relativeEnd, len); + var count = final - k; + var c = o.constructor; + var a = new c(count); + var n = 0; + while (k < final) { + var kValue = o._getter(k); + a._setter(n, kValue); + ++k; + ++n; + } + return a; + }}); + + // %TypedArray%.prototype.some ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'some', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (callbackfn.call(thisp, t._getter(i), i, t)) { + return true; + } + } + return false; + }}); + + // %TypedArray%.prototype.sort ( comparefn ) + Object.defineProperty($TypedArray$.prototype, 'sort', {value: function(comparefn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var tmp = Array(len); + for (var i = 0; i < len; ++i) + tmp[i] = t._getter(i); + if (comparefn) tmp.sort(comparefn); else tmp.sort(); // Hack for IE8/9 + for (i = 0; i < len; ++i) + t._setter(i, tmp[i]); + return t; + }}); + + // %TypedArray%.prototype.subarray(begin = 0, end = this.length ) + // WebIDL: TypedArray subarray(long begin, optional long end); + Object.defineProperty($TypedArray$.prototype, 'subarray', {value: function(start, end) { + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } + + start = ToInt32(start); + end = ToInt32(end); + + if (arguments.length < 1) { start = 0; } + if (arguments.length < 2) { end = this.length; } + + if (start < 0) { start = this.length + start; } + if (end < 0) { end = this.length + end; } + + start = clamp(start, 0, this.length); + end = clamp(end, 0, this.length); + + var len = end - start; + if (len < 0) { + len = 0; + } + + return new this.constructor( + this.buffer, this.byteOffset + start * this.BYTES_PER_ELEMENT, len); + }}); + + // %TypedArray%.prototype.toLocaleString ( ) + // %TypedArray%.prototype.toString ( ) + // %TypedArray%.prototype.values ( ) + // %TypedArray%.prototype [ @@iterator ] ( ) + // get %TypedArray%.prototype [ @@toStringTag ] + // -- defined in es6.js to shim browsers w/ native TypedArrays + + function makeTypedArray(elementSize, pack, unpack) { + // Each TypedArray type requires a distinct constructor instance with + // identical logic, which this produces. + var TypedArray = function() { + Object.defineProperty(this, 'constructor', {value: TypedArray}); + $TypedArray$.apply(this, arguments); + makeArrayAccessors(this); + }; + if ('__proto__' in TypedArray) { + TypedArray.__proto__ = $TypedArray$; + } else { + TypedArray.from = $TypedArray$.from; + TypedArray.of = $TypedArray$.of; + } + + TypedArray.BYTES_PER_ELEMENT = elementSize; + + var TypedArrayPrototype = function() {}; + TypedArrayPrototype.prototype = $TypedArrayPrototype$; + + TypedArray.prototype = new TypedArrayPrototype(); + + Object.defineProperty(TypedArray.prototype, 'BYTES_PER_ELEMENT', {value: elementSize}); + Object.defineProperty(TypedArray.prototype, '_pack', {value: pack}); + Object.defineProperty(TypedArray.prototype, '_unpack', {value: unpack}); + + return TypedArray; + } + + var Int8Array = makeTypedArray(1, packI8, unpackI8); + var Uint8Array = makeTypedArray(1, packU8, unpackU8); + var Uint8ClampedArray = makeTypedArray(1, packU8Clamped, unpackU8); + var Int16Array = makeTypedArray(2, packI16, unpackI16); + var Uint16Array = makeTypedArray(2, packU16, unpackU16); + var Int32Array = makeTypedArray(4, packI32, unpackI32); + var Uint32Array = makeTypedArray(4, packU32, unpackU32); + var Float32Array = makeTypedArray(4, packF32, unpackF32); + var Float64Array = makeTypedArray(8, packF64, unpackF64); + + global.Int8Array = global.Int8Array || Int8Array; + global.Uint8Array = global.Uint8Array || Uint8Array; + global.Uint8ClampedArray = global.Uint8ClampedArray || Uint8ClampedArray; + global.Int16Array = global.Int16Array || Int16Array; + global.Uint16Array = global.Uint16Array || Uint16Array; + global.Int32Array = global.Int32Array || Int32Array; + global.Uint32Array = global.Uint32Array || Uint32Array; + global.Float32Array = global.Float32Array || Float32Array; + global.Float64Array = global.Float64Array || Float64Array; + }()); + + // + // 6 The DataView View Type + // + + (function() { + function r(array, index) { + return IsCallable(array.get) ? array.get(index) : array[index]; + } + + var IS_BIG_ENDIAN = (function() { + var u16array = new Uint16Array([0x1234]), + u8array = new Uint8Array(u16array.buffer); + return r(u8array, 0) === 0x12; + }()); + + // DataView(buffer, byteOffset=0, byteLength=undefined) + // WebIDL: Constructor(ArrayBuffer buffer, + // optional unsigned long byteOffset, + // optional unsigned long byteLength) + function DataView(buffer, byteOffset, byteLength) { + if (!(buffer instanceof ArrayBuffer || Class(buffer) === 'ArrayBuffer')) throw TypeError(); + + byteOffset = ToUint32(byteOffset); + if (byteOffset > buffer.byteLength) + throw RangeError('byteOffset out of range'); + + if (byteLength === undefined) + byteLength = buffer.byteLength - byteOffset; + else + byteLength = ToUint32(byteLength); + + if ((byteOffset + byteLength) > buffer.byteLength) + throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); + + Object.defineProperty(this, 'buffer', {value: buffer}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: byteOffset}); + }; + + // get DataView.prototype.buffer + // get DataView.prototype.byteLength + // get DataView.prototype.byteOffset + // -- applied directly to instances by the constructor + + function makeGetter(arrayType) { + return function GetViewValue(byteOffset, littleEndian) { + byteOffset = ToUint32(byteOffset); + + if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) + throw RangeError('Array index out of range'); + + byteOffset += this.byteOffset; + + var uint8Array = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT), + bytes = []; + for (var i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) + bytes.push(r(uint8Array, i)); + + if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) + bytes.reverse(); + + return r(new arrayType(new Uint8Array(bytes).buffer), 0); + }; + } + + Object.defineProperty(DataView.prototype, 'getUint8', {value: makeGetter(Uint8Array)}); + Object.defineProperty(DataView.prototype, 'getInt8', {value: makeGetter(Int8Array)}); + Object.defineProperty(DataView.prototype, 'getUint16', {value: makeGetter(Uint16Array)}); + Object.defineProperty(DataView.prototype, 'getInt16', {value: makeGetter(Int16Array)}); + Object.defineProperty(DataView.prototype, 'getUint32', {value: makeGetter(Uint32Array)}); + Object.defineProperty(DataView.prototype, 'getInt32', {value: makeGetter(Int32Array)}); + Object.defineProperty(DataView.prototype, 'getFloat32', {value: makeGetter(Float32Array)}); + Object.defineProperty(DataView.prototype, 'getFloat64', {value: makeGetter(Float64Array)}); + + function makeSetter(arrayType) { + return function SetViewValue(byteOffset, value, littleEndian) { + byteOffset = ToUint32(byteOffset); + if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) + throw RangeError('Array index out of range'); + + // Get bytes + var typeArray = new arrayType([value]), + byteArray = new Uint8Array(typeArray.buffer), + bytes = [], i, byteView; + + for (i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) + bytes.push(r(byteArray, i)); + + // Flip if necessary + if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) + bytes.reverse(); + + // Write them + byteView = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT); + byteView.set(bytes); + }; + } + + Object.defineProperty(DataView.prototype, 'setUint8', {value: makeSetter(Uint8Array)}); + Object.defineProperty(DataView.prototype, 'setInt8', {value: makeSetter(Int8Array)}); + Object.defineProperty(DataView.prototype, 'setUint16', {value: makeSetter(Uint16Array)}); + Object.defineProperty(DataView.prototype, 'setInt16', {value: makeSetter(Int16Array)}); + Object.defineProperty(DataView.prototype, 'setUint32', {value: makeSetter(Uint32Array)}); + Object.defineProperty(DataView.prototype, 'setInt32', {value: makeSetter(Int32Array)}); + Object.defineProperty(DataView.prototype, 'setFloat32', {value: makeSetter(Float32Array)}); + Object.defineProperty(DataView.prototype, 'setFloat64', {value: makeSetter(Float64Array)}); + + global.DataView = global.DataView || DataView; + + }()); + +}(this)); diff --git a/daemon/jskitmanager.cpp b/daemon/jskitmanager.cpp new file mode 100644 index 0000000..f6a3f24 --- /dev/null +++ b/daemon/jskitmanager.cpp @@ -0,0 +1,205 @@ +#include <QFile> +#include <QDir> + +#include "jskitmanager.h" +#include "jskitobjects.h" + +JSKitManager::JSKitManager(WatchConnector *watch, AppManager *apps, AppMsgManager *appmsg, Settings *settings, QObject *parent) : + QObject(parent), l(metaObject()->className()), + _watch(watch), _apps(apps), _appmsg(appmsg), _settings(settings), _engine(0) +{ + connect(_appmsg, &AppMsgManager::appStarted, this, &JSKitManager::handleAppStarted); + connect(_appmsg, &AppMsgManager::appStopped, this, &JSKitManager::handleAppStopped); +} + +JSKitManager::~JSKitManager() +{ + if (_engine) { + stopJsApp(); + } +} + +QJSEngine * JSKitManager::engine() +{ + return _engine; +} + +bool JSKitManager::isJSKitAppRunning() const +{ + return _engine != 0; +} + +QString JSKitManager::describeError(QJSValue error) +{ + return QString("%1:%2: %3") + .arg(error.property("fileName").toString()) + .arg(error.property("lineNumber").toInt()) + .arg(error.toString()); +} + +void JSKitManager::showConfiguration() +{ + if (_engine) { + qCDebug(l) << "requesting configuration"; + _jspebble->invokeCallbacks("showConfiguration"); + } else { + qCWarning(l) << "requested to show configuration, but JS engine is not running"; + } +} + +void JSKitManager::handleWebviewClosed(const QString &result) +{ + if (_engine) { + QJSValue eventObj = _engine->newObject(); + eventObj.setProperty("response", _engine->toScriptValue(result)); + + qCDebug(l) << "webview closed with the following result: " << result; + + _jspebble->invokeCallbacks("webviewclosed", QJSValueList({eventObj})); + } else { + qCWarning(l) << "webview closed event, but JS engine is not running"; + } +} + +void JSKitManager::handleAppStarted(const QUuid &uuid) +{ + AppInfo info = _apps->info(uuid); + if (!info.uuid().isNull() && info.isJSKit()) { + qCDebug(l) << "Preparing to start JSKit app" << info.uuid() << info.shortName(); + _curApp = info; + startJsApp(); + } +} + +void JSKitManager::handleAppStopped(const QUuid &uuid) +{ + if (!_curApp.uuid().isNull()) { + if (_curApp.uuid() != uuid) { + qCWarning(l) << "Closed app with invalid UUID"; + } + + stopJsApp(); + _curApp.setUuid(QUuid()); // Clear the uuid to force invalid app + } +} + +void JSKitManager::handleAppMessage(const QUuid &uuid, const QVariantMap &msg) +{ + if (_curApp.uuid() == uuid) { + qCDebug(l) << "received a message for the current JSKit app"; + + if (!_engine) { + qCDebug(l) << "but engine is stopped"; + return; + } + + QJSValue eventObj = _engine->newObject(); + eventObj.setProperty("payload", _engine->toScriptValue(msg)); + + _jspebble->invokeCallbacks("appmessage", QJSValueList({eventObj})); + } +} + +bool JSKitManager::loadJsFile(const QString &filename) +{ + Q_ASSERT(_engine); + + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(l) << "Failed to load JS file:" << file.fileName(); + return false; + } + + qCDebug(l) << "now parsing" << file.fileName(); + + QJSValue result = _engine->evaluate(QString::fromUtf8(file.readAll()), file.fileName()); + if (result.isError()) { + qCWarning(l) << "error while evaluating JS script:" << describeError(result); + return false; + } + + qCDebug(l) << "JS script evaluated"; + + return true; +} + +void JSKitManager::startJsApp() +{ + if (_engine) stopJsApp(); + if (_curApp.uuid().isNull()) { + qCWarning(l) << "Attempting to start JS app with invalid UUID"; + return; + } + + _engine = new QJSEngine(this); + _jspebble = new JSKitPebble(_curApp, this); + _jsconsole = new JSKitConsole(this); + _jsstorage = new JSKitLocalStorage(_curApp.uuid(), this); + _jsgeo = new JSKitGeolocation(this); + + qCDebug(l) << "starting JS app"; + + QJSValue globalObj = _engine->globalObject(); + + globalObj.setProperty("Pebble", _engine->newQObject(_jspebble)); + globalObj.setProperty("console", _engine->newQObject(_jsconsole)); + globalObj.setProperty("localStorage", _engine->newQObject(_jsstorage)); + + QJSValue navigatorObj = _engine->newObject(); + navigatorObj.setProperty("geolocation", _engine->newQObject(_jsgeo)); + navigatorObj.setProperty("language", _engine->toScriptValue(QLocale().name())); + globalObj.setProperty("navigator", navigatorObj); + + // Set this.window = this + globalObj.setProperty("window", globalObj); + + // Shims for compatibility... + QJSValue result = _engine->evaluate( + "function XMLHttpRequest() { return Pebble.createXMLHttpRequest(); }\n" + ); + Q_ASSERT(!result.isError()); + + // Polyfills... + loadJsFile("/usr/share/pebble/js/typedarray.js"); + + // Now load the actual script + loadJsFile(_curApp.path() + "/pebble-js-app.js"); + + // Setup the message callback + QUuid uuid = _curApp.uuid(); + _appmsg->setMessageHandler(uuid, [this, uuid](const QVariantMap &msg) { + QMetaObject::invokeMethod(this, "handleAppMessage", Qt::QueuedConnection, + Q_ARG(QUuid, uuid), + Q_ARG(QVariantMap, msg)); + + // Invoke the slot as a queued connection to give time for the ACK message + // to go through first. + + return true; + }); + + // We try to invoke the callbacks even if script parsing resulted in error... + _jspebble->invokeCallbacks("ready"); +} + +void JSKitManager::stopJsApp() +{ + if (!_engine) return; // Nothing to do! + + qCDebug(l) << "stopping JS app"; + + if (!_curApp.uuid().isNull()) { + _appmsg->clearMessageHandler(_curApp.uuid()); + } + + _engine->collectGarbage(); + + _engine->deleteLater(); + _engine = 0; + _jsstorage->deleteLater(); + _jsstorage = 0; + _jsgeo->deleteLater(); + _jsgeo = 0; + _jspebble->deleteLater(); + _jspebble = 0; +} diff --git a/daemon/jskitmanager.h b/daemon/jskitmanager.h new file mode 100644 index 0000000..4482f34 --- /dev/null +++ b/daemon/jskitmanager.h @@ -0,0 +1,60 @@ +#ifndef JSKITMANAGER_H +#define JSKITMANAGER_H + +#include <QJSEngine> +#include "appmanager.h" +#include "appmsgmanager.h" +#include "settings.h" + +class JSKitPebble; +class JSKitConsole; +class JSKitLocalStorage; +class JSKitGeolocation; + +class JSKitManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitManager(WatchConnector *watch, AppManager *apps, AppMsgManager *appmsg, Settings *settings, QObject *parent = 0); + ~JSKitManager(); + + QJSEngine * engine(); + bool isJSKitAppRunning() const; + + static QString describeError(QJSValue error); + + void showConfiguration(); + void handleWebviewClosed(const QString &result); + +signals: + void appNotification(const QUuid &uuid, const QString &title, const QString &body); + void appOpenUrl(const QUrl &url); + +private slots: + void handleAppStarted(const QUuid &uuid); + void handleAppStopped(const QUuid &uuid); + void handleAppMessage(const QUuid &uuid, const QVariantMap &msg); + +private: + bool loadJsFile(const QString &filename); + void startJsApp(); + void stopJsApp(); + +private: + friend class JSKitPebble; + + WatchConnector *_watch; + AppManager *_apps; + AppMsgManager *_appmsg; + Settings *_settings; + AppInfo _curApp; + QJSEngine *_engine; + QPointer<JSKitPebble> _jspebble; + QPointer<JSKitConsole> _jsconsole; + QPointer<JSKitLocalStorage> _jsstorage; + QPointer<JSKitGeolocation> _jsgeo; +}; + +#endif // JSKITMANAGER_H diff --git a/daemon/jskitobjects.cpp b/daemon/jskitobjects.cpp new file mode 100644 index 0000000..2aca027 --- /dev/null +++ b/daemon/jskitobjects.cpp @@ -0,0 +1,773 @@ +#include <QStandardPaths> +#include <QDesktopServices> +#include <QUrl> +#include <QAuthenticator> +#include <QBuffer> +#include <QDir> +#include <QCryptographicHash> +#include <limits> +#include "jskitobjects.h" + +static const char *token_salt = "0feeb7416d3c4546a19b04bccd8419b1"; + +JSKitPebble::JSKitPebble(const AppInfo &info, JSKitManager *mgr) + : QObject(mgr), l(metaObject()->className()), _appInfo(info), _mgr(mgr) +{ +} + +void JSKitPebble::addEventListener(const QString &type, QJSValue function) +{ + _callbacks[type].append(function); +} + +void JSKitPebble::removeEventListener(const QString &type, QJSValue function) +{ + if (!_callbacks.contains(type)) return; + QList<QJSValue> &callbacks = _callbacks[type]; + + for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ) { + if (it->strictlyEquals(function)) { + it = callbacks.erase(it); + } else { + ++it; + } + } + + if (callbacks.empty()) { + _callbacks.remove(type); + } +} + +uint JSKitPebble::sendAppMessage(QJSValue message, QJSValue callbackForAck, QJSValue callbackForNack) +{ + QVariantMap data = message.toVariant().toMap(); + QPointer<JSKitPebble> pebbObj = this; + uint transactionId = _mgr->_appmsg->nextTransactionId(); + + qCDebug(l) << "sendAppMessage" << data; + + _mgr->_appmsg->send(_appInfo.uuid(), data, + [pebbObj, transactionId, callbackForAck]() mutable { + if (pebbObj.isNull()) return; + if (callbackForAck.isCallable()) { + qCDebug(pebbObj->l) << "Invoking ack callback"; + QJSValue event = pebbObj->buildAckEventObject(transactionId); + QJSValue result = callbackForAck.call(QJSValueList({event})); + if (result.isError()) { + qCWarning(pebbObj->l) << "error while invoking ACK callback" << callbackForAck.toString() << ":" + << JSKitManager::describeError(result); + } + } else { + qCDebug(pebbObj->l) << "Ack callback not callable"; + } + }, + [pebbObj, transactionId, callbackForNack]() mutable { + if (pebbObj.isNull()) return; + if (callbackForNack.isCallable()) { + qCDebug(pebbObj->l) << "Invoking nack callback"; + QJSValue event = pebbObj->buildAckEventObject(transactionId, "NACK from watch"); + QJSValue result = callbackForNack.call(QJSValueList({event})); + if (result.isError()) { + qCWarning(pebbObj->l) << "error while invoking NACK callback" << callbackForNack.toString() << ":" + << JSKitManager::describeError(result); + } + } else { + qCDebug(pebbObj->l) << "Nack callback not callable"; + } + }); + + return transactionId; +} + +void JSKitPebble::showSimpleNotificationOnPebble(const QString &title, const QString &body) +{ + qCDebug(l) << "showSimpleNotificationOnPebble" << title << body; + emit _mgr->appNotification(_appInfo.uuid(), title, body); +} + +void JSKitPebble::openURL(const QUrl &url) +{ + qCDebug(l) << "opening url" << url.toString(); + emit _mgr->appOpenUrl(url); +} + +QString JSKitPebble::getAccountToken() const +{ + // We do not have any account system, so we just fake something up. + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(token_salt, strlen(token_salt)); + hasher.addData(_appInfo.uuid().toByteArray()); + + QString token = _mgr->_settings->property("accountToken").toString(); + if (token.isEmpty()) { + token = QUuid::createUuid().toString(); + qCDebug(l) << "created new account token" << token; + _mgr->_settings->setProperty("accountToken", token); + } + hasher.addData(token.toLatin1()); + + QString hash = hasher.result().toHex(); + qCDebug(l) << "returning account token" << hash; + + return hash; +} + +QString JSKitPebble::getWatchToken() const +{ + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(token_salt, strlen(token_salt)); + hasher.addData(_appInfo.uuid().toByteArray()); + hasher.addData(_mgr->_watch->serialNumber().toLatin1()); + + QString hash = hasher.result().toHex(); + qCDebug(l) << "returning watch token" << hash; + + return hash; +} + +QJSValue JSKitPebble::createXMLHttpRequest() +{ + JSKitXMLHttpRequest *xhr = new JSKitXMLHttpRequest(_mgr, 0); + // Should be deleted by JS engine. + return _mgr->engine()->newQObject(xhr); +} + +QJSValue JSKitPebble::buildAckEventObject(uint transaction, const QString &message) const +{ + QJSEngine *engine = _mgr->engine(); + QJSValue eventObj = engine->newObject(); + QJSValue dataObj = engine->newObject(); + + dataObj.setProperty("transactionId", engine->toScriptValue(transaction)); + eventObj.setProperty("data", dataObj); + + if (!message.isEmpty()) { + QJSValue errorObj = engine->newObject(); + errorObj.setProperty("message", engine->toScriptValue(message)); + eventObj.setProperty("error", errorObj); + } + + return eventObj; +} + +void JSKitPebble::invokeCallbacks(const QString &type, const QJSValueList &args) +{ + if (!_callbacks.contains(type)) return; + QList<QJSValue> &callbacks = _callbacks[type]; + + for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ++it) { + qCDebug(l) << "invoking callback" << type << it->toString(); + QJSValue result = it->call(args); + if (result.isError()) { + qCWarning(l) << "error while invoking callback" << type << it->toString() << ":" + << JSKitManager::describeError(result); + } + } +} + +JSKitConsole::JSKitConsole(JSKitManager *mgr) + : QObject(mgr), l(metaObject()->className()) +{ +} + +void JSKitConsole::log(const QString &msg) +{ + qCDebug(l) << msg; +} + +JSKitLocalStorage::JSKitLocalStorage(const QUuid &uuid, JSKitManager *mgr) + : QObject(mgr), _storage(new QSettings(getStorageFileFor(uuid), QSettings::IniFormat, this)) +{ + _len = _storage->allKeys().size(); +} + +int JSKitLocalStorage::length() const +{ + return _len; +} + +QJSValue JSKitLocalStorage::getItem(const QString &key) const +{ + QVariant value = _storage->value(key); + if (value.isValid()) { + return QJSValue(value.toString()); + } else { + return QJSValue(QJSValue::NullValue); + } +} + +void JSKitLocalStorage::setItem(const QString &key, const QString &value) +{ + _storage->setValue(key, QVariant::fromValue(value)); + checkLengthChanged(); +} + +void JSKitLocalStorage::removeItem(const QString &key) +{ + _storage->remove(key); + checkLengthChanged(); +} + +void JSKitLocalStorage::clear() +{ + _storage->clear(); + _len = 0; + emit lengthChanged(); +} + +void JSKitLocalStorage::checkLengthChanged() +{ + int curLen = _storage->allKeys().size(); + if (_len != curLen) { + _len = curLen; + emit lengthChanged(); + } +} + +QString JSKitLocalStorage::getStorageFileFor(const QUuid &uuid) +{ + QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); + dataDir.mkdir("js-storage"); + QString fileName = uuid.toString(); + fileName.remove('{'); + fileName.remove('}'); + return dataDir.absoluteFilePath("js-storage/" + fileName + ".ini"); +} + +JSKitXMLHttpRequest::JSKitXMLHttpRequest(JSKitManager *mgr, QObject *parent) + : QObject(parent), l(metaObject()->className()), _mgr(mgr), + _net(new QNetworkAccessManager(this)), _timeout(0), _reply(0) +{ + qCDebug(l) << "constructed"; + connect(_net, &QNetworkAccessManager::authenticationRequired, + this, &JSKitXMLHttpRequest::handleAuthenticationRequired); +} + +JSKitXMLHttpRequest::~JSKitXMLHttpRequest() +{ + qCDebug(l) << "destructed"; +} + +void JSKitXMLHttpRequest::open(const QString &method, const QString &url, bool async, const QString &username, const QString &password) +{ + if (_reply) { + _reply->deleteLater(); + _reply = 0; + } + + _username = username; + _password = password; + _request = QNetworkRequest(QUrl(url)); + _verb = method; + Q_UNUSED(async); + + qCDebug(l) << "opened to URL" << _request.url().toString(); +} + +void JSKitXMLHttpRequest::setRequestHeader(const QString &header, const QString &value) +{ + qCDebug(l) << "setRequestHeader" << header << value; + _request.setRawHeader(header.toLatin1(), value.toLatin1()); +} + +void JSKitXMLHttpRequest::send(const QJSValue &data) +{ + QByteArray byteData; + + if (data.isUndefined() || data.isNull()) { + // Do nothing, byteData is empty. + } else if (data.isString()) { + byteData == data.toString().toUtf8(); + } else if (data.isObject()) { + if (data.hasProperty("byteLength")) { + // Looks like an ArrayView or an ArrayBufferView! + QJSValue buffer = data.property("buffer"); + if (buffer.isUndefined()) { + // We must assume we've been passed an ArrayBuffer directly + buffer = data; + } + + QJSValue array = data.property("_bytes"); + int byteLength = data.property("byteLength").toInt(); + + if (array.isArray()) { + byteData.reserve(byteLength); + + for (int i = 0; i < byteLength; i++) { + byteData.append(array.property(i).toInt()); + } + + qCDebug(l) << "passed an ArrayBufferView of" << byteData.length() << "bytes"; + } else { + qCWarning(l) << "passed an unknown/invalid ArrayBuffer" << data.toString(); + } + } else { + qCWarning(l) << "passed an unknown object" << data.toString(); + } + + } + + QBuffer *buffer; + if (!byteData.isEmpty()) { + buffer = new QBuffer; + buffer->setData(byteData); + } else { + buffer = 0; + } + + qCDebug(l) << "sending" << _verb << "to" << _request.url() << "with" << QString::fromUtf8(byteData); + _reply = _net->sendCustomRequest(_request, _verb.toLatin1(), buffer); + + connect(_reply, &QNetworkReply::finished, + this, &JSKitXMLHttpRequest::handleReplyFinished); + connect(_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), + this, &JSKitXMLHttpRequest::handleReplyError); + + if (buffer) { + // So that it gets deleted alongside the reply object. + buffer->setParent(_reply); + } +} + +void JSKitXMLHttpRequest::abort() +{ + if (_reply) { + _reply->deleteLater(); + _reply = 0; + } +} + +QJSValue JSKitXMLHttpRequest::onload() const +{ + return _onload; +} + +void JSKitXMLHttpRequest::setOnload(const QJSValue &value) +{ + _onload = value; +} + +QJSValue JSKitXMLHttpRequest::ontimeout() const +{ + return _ontimeout; +} + +void JSKitXMLHttpRequest::setOntimeout(const QJSValue &value) +{ + _ontimeout = value; +} + +QJSValue JSKitXMLHttpRequest::onerror() const +{ + return _onerror; +} + +void JSKitXMLHttpRequest::setOnerror(const QJSValue &value) +{ + _onerror = value; +} + +uint JSKitXMLHttpRequest::readyState() const +{ + if (!_reply) { + return UNSENT; + } else if (_reply->isFinished()) { + return DONE; + } else { + return LOADING; + } +} + +uint JSKitXMLHttpRequest::timeout() const +{ + return _timeout; +} + +void JSKitXMLHttpRequest::setTimeout(uint value) +{ + _timeout = value; + // TODO Handle fetch in-progress. +} + +uint JSKitXMLHttpRequest::status() const +{ + if (!_reply || !_reply->isFinished()) { + return 0; + } else { + return _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt(); + } +} + +QString JSKitXMLHttpRequest::statusText() const +{ + if (!_reply || !_reply->isFinished()) { + return QString(); + } else { + return _reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + } +} + +QString JSKitXMLHttpRequest::responseType() const +{ + return _responseType; +} + +void JSKitXMLHttpRequest::setResponseType(const QString &type) +{ + qCDebug(l) << "response type set to" << type; + _responseType = type; +} + +QJSValue JSKitXMLHttpRequest::response() const +{ + QJSEngine *engine = _mgr->engine(); + if (_responseType.isEmpty() || _responseType == "text") { + return engine->toScriptValue(QString::fromUtf8(_response)); + } else if (_responseType == "arraybuffer") { + QJSValue arrayBufferProto = engine->globalObject().property("ArrayBuffer").property("prototype"); + QJSValue arrayBuf = engine->newObject(); + if (!arrayBufferProto.isUndefined()) { + arrayBuf.setPrototype(arrayBufferProto); + arrayBuf.setProperty("byteLength", engine->toScriptValue<uint>(_response.size())); + QJSValue array = engine->newArray(_response.size()); + for (int i = 0; i < _response.size(); i++) { + array.setProperty(i, engine->toScriptValue<int>(_response[i])); + } + arrayBuf.setProperty("_bytes", array); + qCDebug(l) << "returning ArrayBuffer of" << _response.size() << "bytes"; + } else { + qCWarning(l) << "Cannot find proto of ArrayBuffer"; + } + return arrayBuf; + } else { + qCWarning(l) << "unsupported responseType:" << _responseType; + return engine->toScriptValue<void*>(0); + } +} + +QString JSKitXMLHttpRequest::responseText() const +{ + return QString::fromUtf8(_response); +} + +void JSKitXMLHttpRequest::handleReplyFinished() +{ + if (!_reply) { + qCDebug(l) << "reply finished too late"; + return; + } + + _response = _reply->readAll(); + qCDebug(l) << "reply finished, reply text:" << QString::fromUtf8(_response); + + emit readyStateChanged(); + emit statusChanged(); + emit statusTextChanged(); + emit responseChanged(); + emit responseTextChanged(); + + if (_onload.isCallable()) { + qCDebug(l) << "going to call onload handler:" << _onload.toString(); + QJSValue result = _onload.callWithInstance(_mgr->engine()->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onload handler:" << JSKitManager::describeError(result); + } + } else { + qCDebug(l) << "No onload set"; + } +} + +void JSKitXMLHttpRequest::handleReplyError(QNetworkReply::NetworkError code) +{ + if (!_reply) { + qCDebug(l) << "reply error too late"; + return; + } + + qCDebug(l) << "reply error" << code; + + emit readyStateChanged(); + emit statusChanged(); + emit statusTextChanged(); + + if (_onerror.isCallable()) { + qCDebug(l) << "going to call onerror handler:" << _onload.toString(); + QJSValue result = _onerror.callWithInstance(_mgr->engine()->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onerror handler:" << JSKitManager::describeError(result); + } + } +} + +void JSKitXMLHttpRequest::handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth) +{ + if (_reply == reply) { + qCDebug(l) << "authentication required"; + + if (!_username.isEmpty() || !_password.isEmpty()) { + qCDebug(l) << "using provided authorization:" << _username; + + auth->setUser(_username); + auth->setPassword(_password); + } else { + qCDebug(l) << "no username or password provided"; + } + } +} + +JSKitGeolocation::JSKitGeolocation(JSKitManager *mgr) + : QObject(mgr), l(metaObject()->className()), + _mgr(mgr), _source(0), _lastWatchId(0) +{ +} + +void JSKitGeolocation::getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options) +{ + setupWatcher(successCallback, errorCallback, options, true); +} + +int JSKitGeolocation::watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options) +{ + return setupWatcher(successCallback, errorCallback, options, false); +} + +void JSKitGeolocation::clearWatch(int watchId) +{ + removeWatcher(watchId); +} + +void JSKitGeolocation::handleError(QGeoPositionInfoSource::Error error) +{ + qCWarning(l) << "positioning error: " << error; + // TODO +} + +void JSKitGeolocation::handlePosition(const QGeoPositionInfo &pos) +{ + qCDebug(l) << "got position at" << pos.timestamp() << "type" << pos.coordinate().type(); + + if (_watches.empty()) { + qCWarning(l) << "got position update but no one is watching"; + _source->stopUpdates(); // Just in case. + return; + } + + QJSValue obj = buildPositionObject(pos); + + for (auto it = _watches.begin(); it != _watches.end(); /*no adv*/) { + invokeCallback(it->successCallback, obj); + + if (it->once) { + it = _watches.erase(it); + } else { + it->timer.restart(); + ++it; + } + } +} + +void JSKitGeolocation::handleTimeout() +{ + qCDebug(l) << "positioning timeout"; + + if (_watches.empty()) { + qCWarning(l) << "got position timeout but no one is watching"; + _source->stopUpdates(); + return; + } + + QJSValue obj = buildPositionErrorObject(TIMEOUT, "timeout"); + + for (auto it = _watches.begin(); it != _watches.end(); /*no adv*/) { + if (it->timer.hasExpired(it->timeout)) { + qCDebug(l) << "positioning timeout for watch" << it->watchId + << ", watch is" << it->timer.elapsed() << "ms old, timeout is" << it->timeout; + invokeCallback(it->errorCallback, obj); + + if (it->once) { + it = _watches.erase(it); + } else { + it->timer.restart(); + ++it; + } + } else { + ++it; + } + } + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); +} + +void JSKitGeolocation::updateTimeouts() +{ + int once_timeout = -1, updates_timeout = -1; + + qCDebug(l) << Q_FUNC_INFO; + + Q_FOREACH(const Watcher &watcher, _watches) { + qint64 rem_timeout = watcher.timeout - watcher.timer.elapsed(); + qCDebug(l) << "watch" << watcher.watchId << "rem timeout" << rem_timeout; + if (rem_timeout >= 0) { + // In case it is too large... + rem_timeout = qMin<qint64>(rem_timeout, std::numeric_limits<int>::max()); + if (watcher.once) { + once_timeout = once_timeout >= 0 ? qMin<int>(once_timeout, rem_timeout) : rem_timeout; + } else { + updates_timeout = updates_timeout >= 0 ? qMin<int>(updates_timeout, rem_timeout) : rem_timeout; + } + } + } + + if (updates_timeout >= 0) { + qCDebug(l) << "setting location update interval to" << updates_timeout; + _source->setUpdateInterval(updates_timeout); + _source->startUpdates(); + } else { + qCDebug(l) << "stopping updates"; + _source->stopUpdates(); + } + + if (once_timeout >= 0) { + qCDebug(l) << "requesting single location update with timeout" << once_timeout; + _source->requestUpdate(once_timeout); + } +} + +int JSKitGeolocation::setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once) +{ + Watcher watcher; + watcher.successCallback = successCallback; + watcher.errorCallback = errorCallback; + watcher.highAccuracy = options.value("enableHighAccuracy").toBool(); + watcher.timeout = options.value("timeout", std::numeric_limits<int>::max() - 1).toInt(); + watcher.once = once; + watcher.watchId = ++_lastWatchId; + + qlonglong maximumAge = options.value("maximumAge", 0).toLongLong(); + + qCDebug(l) << "setting up watcher, gps=" << watcher.highAccuracy << "timeout=" << watcher.timeout << "maximumAge=" << maximumAge << "once=" << once; + + if (!_source) { + _source = QGeoPositionInfoSource::createDefaultSource(this); + connect(_source, static_cast<void (QGeoPositionInfoSource::*)(QGeoPositionInfoSource::Error)>(&QGeoPositionInfoSource::error), + this, &JSKitGeolocation::handleError); + connect(_source, &QGeoPositionInfoSource::positionUpdated, + this, &JSKitGeolocation::handlePosition); + connect(_source, &QGeoPositionInfoSource::updateTimeout, + this, &JSKitGeolocation::handleTimeout); + } + + if (maximumAge > 0) { + QDateTime threshold = QDateTime::currentDateTime().addMSecs(-qint64(maximumAge)); + QGeoPositionInfo pos = _source->lastKnownPosition(watcher.highAccuracy); + qCDebug(l) << "got pos timestamp" << pos.timestamp() << " but we want" << threshold; + if (pos.isValid() && pos.timestamp() >= threshold) { + invokeCallback(watcher.successCallback, buildPositionObject(pos)); + if (once) { + return -1; + } + } else if (watcher.timeout == 0 && once) { + // If the timeout has already expired, and we have no cached data + // Do not even bother to turn on the GPS; return error object now. + invokeCallback(watcher.errorCallback, buildPositionErrorObject(TIMEOUT, "no cached position")); + return -1; + } + } + + watcher.timer.start(); + _watches.append(watcher); + + qCDebug(l) << "added new watch" << watcher.watchId; + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); + + return watcher.watchId; +} + +void JSKitGeolocation::removeWatcher(int watchId) +{ + Watcher watcher; + + qCDebug(l) << "removing watchId" << watcher.watchId; + + for (int i = 0; i < _watches.size(); i++) { + if (_watches[i].watchId == watchId) { + watcher = _watches.takeAt(i); + break; + } + } + + if (watcher.watchId != watchId) { + qCWarning(l) << "watchId not found"; + return; + } + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); +} + +QJSValue JSKitGeolocation::buildPositionObject(const QGeoPositionInfo &pos) +{ + QJSEngine *engine = _mgr->engine(); + QJSValue obj = engine->newObject(); + QJSValue coords = engine->newObject(); + QJSValue timestamp = engine->toScriptValue<quint64>(pos.timestamp().toMSecsSinceEpoch()); + + coords.setProperty("latitude", engine->toScriptValue(pos.coordinate().latitude())); + coords.setProperty("longitude", engine->toScriptValue(pos.coordinate().longitude())); + if (pos.coordinate().type() == QGeoCoordinate::Coordinate3D) { + coords.setProperty("altitude", engine->toScriptValue(pos.coordinate().altitude())); + } else { + coords.setProperty("altitude", engine->toScriptValue<void*>(0)); + } + + coords.setProperty("accuracy", engine->toScriptValue(pos.attribute(QGeoPositionInfo::HorizontalAccuracy))); + + if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) { + coords.setProperty("altitudeAccuracy", engine->toScriptValue(pos.attribute(QGeoPositionInfo::VerticalAccuracy))); + } else { + coords.setProperty("altitudeAccuracy", engine->toScriptValue<void*>(0)); + } + + if (pos.hasAttribute(QGeoPositionInfo::Direction)) { + coords.setProperty("heading", engine->toScriptValue(pos.attribute(QGeoPositionInfo::Direction))); + } else { + coords.setProperty("heading", engine->toScriptValue<void*>(0)); + } + + if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) { + coords.setProperty("speed", engine->toScriptValue(pos.attribute(QGeoPositionInfo::GroundSpeed))); + } else { + coords.setProperty("speed", engine->toScriptValue<void*>(0)); + } + + obj.setProperty("coords", coords); + obj.setProperty("timestamp", timestamp); + + return obj; +} + +QJSValue JSKitGeolocation::buildPositionErrorObject(PositionError error, const QString &message) +{ + QJSEngine *engine = _mgr->engine(); + QJSValue obj = engine->newObject(); + + obj.setProperty("code", engine->toScriptValue<unsigned short>(error)); + obj.setProperty("message", engine->toScriptValue(message)); + + return obj; +} + +void JSKitGeolocation::invokeCallback(QJSValue callback, QJSValue event) +{ + if (callback.isCallable()) { + qCDebug(l) << "invoking callback" << callback.toString(); + QJSValue result = callback.call(QJSValueList({event})); + if (result.isError()) { + qCWarning(l) << "while invoking callback: " << JSKitManager::describeError(result); + } + } else { + qCWarning(l) << "callback is not callable"; + } +} diff --git a/daemon/jskitobjects.h b/daemon/jskitobjects.h new file mode 100644 index 0000000..1477fc6 --- /dev/null +++ b/daemon/jskitobjects.h @@ -0,0 +1,221 @@ +#ifndef JSKITMANAGER_P_H +#define JSKITMANAGER_P_H + +#include <QElapsedTimer> +#include <QSettings> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QGeoPositionInfoSource> +#include "jskitmanager.h" + +class JSKitPebble : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitPebble(const AppInfo &appInfo, JSKitManager *mgr); + + Q_INVOKABLE void addEventListener(const QString &type, QJSValue function); + Q_INVOKABLE void removeEventListener(const QString &type, QJSValue function); + + Q_INVOKABLE uint sendAppMessage(QJSValue message, QJSValue callbackForAck = QJSValue(), QJSValue callbackForNack = QJSValue()); + + Q_INVOKABLE void showSimpleNotificationOnPebble(const QString &title, const QString &body); + + Q_INVOKABLE void openURL(const QUrl &url); + + Q_INVOKABLE QString getAccountToken() const; + Q_INVOKABLE QString getWatchToken() const; + + Q_INVOKABLE QJSValue createXMLHttpRequest(); + + void invokeCallbacks(const QString &type, const QJSValueList &args = QJSValueList()); + +private: + QJSValue buildAckEventObject(uint transaction, const QString &message = QString()) const; + +private: + AppInfo _appInfo; + JSKitManager *_mgr; + QHash<QString, QList<QJSValue>> _callbacks; +}; + +class JSKitConsole : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitConsole(JSKitManager *mgr); + + Q_INVOKABLE void log(const QString &msg); +}; + +class JSKitLocalStorage : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int length READ length NOTIFY lengthChanged) + +public: + explicit JSKitLocalStorage(const QUuid &uuid, JSKitManager *mgr); + + int length() const; + + Q_INVOKABLE QJSValue getItem(const QString &key) const; + Q_INVOKABLE void setItem(const QString &key, const QString &value); + Q_INVOKABLE void removeItem(const QString &key); + + Q_INVOKABLE void clear(); + +signals: + void lengthChanged(); + +private: + void checkLengthChanged(); + static QString getStorageFileFor(const QUuid &uuid); + +private: + QSettings *_storage; + int _len; +}; + +class JSKitXMLHttpRequest : public QObject +{ + Q_OBJECT + QLoggingCategory l; + Q_ENUMS(ReadyStates) + + Q_PROPERTY(QJSValue onload READ onload WRITE setOnload) + Q_PROPERTY(QJSValue ontimeout READ ontimeout WRITE setOntimeout) + Q_PROPERTY(QJSValue onerror READ onerror WRITE setOnerror) + Q_PROPERTY(uint readyState READ readyState NOTIFY readyStateChanged) + Q_PROPERTY(uint timeout READ timeout WRITE setTimeout) + Q_PROPERTY(uint status READ status NOTIFY statusChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(QString responseType READ responseType WRITE setResponseType) + Q_PROPERTY(QJSValue response READ response NOTIFY responseChanged) + Q_PROPERTY(QString responseText READ responseText NOTIFY responseTextChanged) + +public: + explicit JSKitXMLHttpRequest(JSKitManager *mgr, QObject *parent = 0); + ~JSKitXMLHttpRequest(); + + enum ReadyStates { + UNSENT = 0, + OPENED = 1, + HEADERS_RECEIVED = 2, + LOADING = 3, + DONE = 4 + }; + + Q_INVOKABLE void open(const QString &method, const QString &url, bool async = false, const QString &username = QString(), const QString &password = QString()); + Q_INVOKABLE void setRequestHeader(const QString &header, const QString &value); + Q_INVOKABLE void send(const QJSValue &data = QJSValue(QJSValue::NullValue)); + Q_INVOKABLE void abort(); + + QJSValue onload() const; + void setOnload(const QJSValue &value); + QJSValue ontimeout() const; + void setOntimeout(const QJSValue &value); + QJSValue onerror() const; + void setOnerror(const QJSValue &value); + + uint readyState() const; + + uint timeout() const; + void setTimeout(uint value); + + uint status() const; + QString statusText() const; + + QString responseType() const; + void setResponseType(const QString& type); + + QJSValue response() const; + QString responseText() const; + +signals: + void readyStateChanged(); + void statusChanged(); + void statusTextChanged(); + void responseChanged(); + void responseTextChanged(); + +private slots: + void handleReplyFinished(); + void handleReplyError(QNetworkReply::NetworkError code); + void handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth); + +private: + JSKitManager *_mgr; + QNetworkAccessManager *_net; + QString _verb; + uint _timeout; + QString _username; + QString _password; + QNetworkRequest _request; + QNetworkReply *_reply; + QString _responseType; + QByteArray _response; + QJSValue _onload; + QJSValue _ontimeout; + QJSValue _onerror; +}; + +class JSKitGeolocation : public QObject +{ + Q_OBJECT + Q_ENUMS(PositionError) + QLoggingCategory l; + + struct Watcher; + +public: + explicit JSKitGeolocation(JSKitManager *mgr); + + enum PositionError { + PERMISSION_DENIED = 1, + POSITION_UNAVAILABLE = 2, + TIMEOUT = 3 + }; + + Q_INVOKABLE void getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap()); + Q_INVOKABLE int watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap()); + Q_INVOKABLE void clearWatch(int watchId); + +private slots: + void handleError(const QGeoPositionInfoSource::Error error); + void handlePosition(const QGeoPositionInfo &pos); + void handleTimeout(); + void updateTimeouts(); + +private: + int setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once); + void removeWatcher(int watchId); + + QJSValue buildPositionObject(const QGeoPositionInfo &pos); + QJSValue buildPositionErrorObject(PositionError error, const QString &message = QString()); + QJSValue buildPositionErrorObject(const QGeoPositionInfoSource::Error error); + void invokeCallback(QJSValue callback, QJSValue event); + +private: + JSKitManager *_mgr; + QGeoPositionInfoSource *_source; + + struct Watcher { + QJSValue successCallback; + QJSValue errorCallback; + int watchId; + bool once; + bool highAccuracy; + int timeout; + QElapsedTimer timer; + }; + + QList<Watcher> _watches; + int _lastWatchId; +}; + +#endif // JSKITMANAGER_P_H diff --git a/daemon/manager.cpp b/daemon/manager.cpp index fc64b63..6498c68 100644 --- a/daemon/manager.cpp +++ b/daemon/manager.cpp @@ -1,13 +1,25 @@ -#include "manager.h" -#include "dbusadaptor.h" - #include <QDebug> #include <QtContacts/QContact> #include <QtContacts/QContactPhoneNumber> -Manager::Manager(watch::WatchConnector *watch, DBusConnector *dbus, VoiceCallManager *voice, NotificationManager *notifications, Settings *settings) : - QObject(0), l(metaObject()->className()), watch(watch), dbus(dbus), voice(voice), notifications(notifications), commands(new WatchCommands(watch, this)), - settings(settings), notification(MNotification::DeviceEvent) +#include "manager.h" +#include "watch_adaptor.h" + +Manager::Manager(Settings *settings, QObject *parent) : + QObject(parent), l(metaObject()->className()), settings(settings), + proxy(new PebbledProxy(this)), + watch(new WatchConnector(this)), + dbus(new DBusConnector(this)), + upload(new UploadManager(watch, this)), + apps(new AppManager(this)), + bank(new BankManager(watch, upload, apps, this)), + voice(new VoiceCallManager(settings, this)), + notifications(new NotificationManager(settings, this)), + music(new MusicManager(watch, this)), + datalog(new DataLogManager(watch, this)), + appmsg(new AppMsgManager(apps, watch, this)), + js(new JSKitManager(watch, apps, appmsg, settings, this)), + notification(MNotification::DeviceEvent) { connect(settings, SIGNAL(valueChanged(QString)), SLOT(onSettingChanged(const QString&))); connect(settings, SIGNAL(valuesChanged()), SLOT(onSettingsChanged())); @@ -22,6 +34,19 @@ Manager::Manager(watch::WatchConnector *watch, DBusConnector *dbus, VoiceCallMan numberFilter.setMatchFlags(QContactFilter::MatchPhoneNumber); connect(watch, SIGNAL(connectedChanged()), SLOT(onConnectedChanged())); + watch->setEndpointHandler(WatchConnector::watchPHONE_VERSION, + [this](const QByteArray& data) { + Q_UNUSED(data); + watch->sendPhoneVersion(); + return true; + }); + watch->setEndpointHandler(WatchConnector::watchPHONE_CONTROL, + [this](const QByteArray &data) { + if (data.at(0) == WatchConnector::callHANGUP) { + voice->hangupAll(); + } + return true; + }); connect(voice, SIGNAL(activeVoiceCallChanged()), SLOT(onActiveVoiceCallChanged())); connect(voice, SIGNAL(error(const QString &)), SLOT(onVoiceError(const QString &))); @@ -32,28 +57,25 @@ Manager::Manager(watch::WatchConnector *watch, DBusConnector *dbus, VoiceCallMan connect(notifications, SIGNAL(twitterNotify(const QString &,const QString &)), SLOT(onTwitterNotify(const QString &,const QString &))); connect(notifications, SIGNAL(facebookNotify(const QString &,const QString &)), SLOT(onFacebookNotify(const QString &,const QString &))); - connect(watch, SIGNAL(messageDecoded(uint,QByteArray)), commands, SLOT(processMessage(uint,QByteArray))); - connect(commands, SIGNAL(hangup()), SLOT(hangupAll())); + connect(appmsg, &AppMsgManager::appStarted, this, &Manager::onAppOpened); + connect(appmsg, &AppMsgManager::appStopped, this, &Manager::onAppClosed); + + connect(js, &JSKitManager::appNotification, this, &Manager::onAppNotification); - PebbledProxy *proxy = new PebbledProxy(this); - PebbledAdaptor *adaptor = new PebbledAdaptor(proxy); QDBusConnection session = QDBusConnection::sessionBus(); - session.registerObject("/", proxy); + new WatchAdaptor(proxy); + session.registerObject("/org/pebbled/Watch", proxy); session.registerService("org.pebbled"); - connect(dbus, SIGNAL(pebbleChanged()), adaptor, SIGNAL(pebbleChanged())); - connect(watch, SIGNAL(connectedChanged()), adaptor, SIGNAL(connectedChanged())); + + connect(dbus, &DBusConnector::pebbleChanged, proxy, &PebbledProxy::NameChanged); + connect(dbus, &DBusConnector::pebbleChanged, proxy, &PebbledProxy::AddressChanged); + connect(watch, &WatchConnector::connectedChanged, proxy, &PebbledProxy::ConnectedChanged); + connect(bank, &BankManager::slotsChanged, proxy, &PebbledProxy::AppSlotsChanged); QString currentProfile = getCurrentProfile(); defaultProfile = currentProfile.isEmpty() ? "ambience" : currentProfile; connect(watch, SIGNAL(connectedChanged()), SLOT(applyProfile())); - // Music Control interface - session.connect("", "/org/mpris/MediaPlayer2", - "org.freedesktop.DBus.Properties", "PropertiesChanged", - this, SLOT(onMprisPropertiesChanged(QString,QMap<QString,QVariant>,QStringList))); - - connect(this, SIGNAL(mprisMetadataChanged(QVariantMap)), commands, SLOT(onMprisMetadataChanged(QVariantMap))); - // Set BT icon for notification notification.setImage("icon-system-bluetooth-device"); @@ -62,7 +84,10 @@ Manager::Manager(watch::WatchConnector *watch, DBusConnector *dbus, VoiceCallMan connect(dbus, SIGNAL(pebbleChanged()), SLOT(onPebbleChanged())); dbus->findPebble(); } +} +Manager::~Manager() +{ } void Manager::onSettingChanged(const QString &key) @@ -99,22 +124,6 @@ void Manager::onConnectedChanged() if (!notification.publish()) { qCDebug(l) << "Failed publishing notification"; } - - if (watch->isConnected()) { - QString mpris = this->mpris(); - if (not mpris.isEmpty()) { - QDBusReply<QDBusVariant> Metadata = QDBusConnection::sessionBus().call( - QDBusMessage::createMethodCall(mpris, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get") - << "org.mpris.MediaPlayer2.Player" << "Metadata"); - if (Metadata.isValid()) { - setMprisMetadata(Metadata.value().variant().value<QDBusArgument>()); - } - else { - qCCritical(l) << Metadata.error().message(); - setMprisMetadata(QVariantMap()); - } - } - } } void Manager::onActiveVoiceCallChanged() @@ -247,60 +256,7 @@ void Manager::onEmailNotify(const QString &sender, const QString &data,const QSt watch->sendEmailNotification(sender, data, subject); } -void Manager::hangupAll() -{ - foreach (VoiceCallHandler* handler, voice->voiceCalls()) { - handler->hangup(); - } -} - -void Manager::onMprisPropertiesChanged(QString interface, QMap<QString,QVariant> changed, QStringList invalidated) -{ - qCDebug(l) << interface << changed << invalidated; - - if (changed.contains("Metadata")) { - setMprisMetadata(changed.value("Metadata").value<QDBusArgument>()); - } - - if (changed.contains("PlaybackStatus")) { - QString PlaybackStatus = changed.value("PlaybackStatus").toString(); - if (PlaybackStatus == "Stopped") { - setMprisMetadata(QVariantMap()); - } - } - - lastSeenMpris = message().service(); - qCDebug(l) << "lastSeenMpris:" << lastSeenMpris; -} - -QString Manager::mpris() -{ - const QStringList &services = dbus->services(); - if (not lastSeenMpris.isEmpty() && services.contains(lastSeenMpris)) - return lastSeenMpris; - - foreach (QString service, services) - if (service.startsWith("org.mpris.MediaPlayer2.")) - return service; - - return QString(); -} - -void Manager::setMprisMetadata(QDBusArgument metadata) -{ - if (metadata.currentType() == QDBusArgument::MapType) { - metadata >> mprisMetadata; - emit mprisMetadataChanged(mprisMetadata); - } -} - -void Manager::setMprisMetadata(QVariantMap metadata) -{ - mprisMetadata = metadata; - emit mprisMetadataChanged(mprisMetadata); -} - -QString Manager::getCurrentProfile() +QString Manager::getCurrentProfile() const { QDBusReply<QString> profile = QDBusConnection::sessionBus().call( QDBusMessage::createMethodCall("com.nokia.profiled", "/com/nokia/profiled", "com.nokia.profiled", "get_profile")); @@ -369,3 +325,187 @@ void Manager::transliterateMessage(const QString &text) qCDebug(l) << "String after transliteration:" << text; } } + +void Manager::onAppNotification(const QUuid &uuid, const QString &title, const QString &body) +{ + Q_UNUSED(uuid); + watch->sendSMSNotification(title, body); +} + +void Manager::onAppOpened(const QUuid &uuid) +{ + currentAppUuid = uuid; + emit proxy->AppUuidChanged(); + emit proxy->AppOpened(uuid.toString()); +} + +void Manager::onAppClosed(const QUuid &uuid) +{ + currentAppUuid = QUuid(); + emit proxy->AppClosed(uuid.toString()); + emit proxy->AppUuidChanged(); +} + +QStringList PebbledProxy::AppSlots() const +{ + const int num_slots = manager()->bank->numSlots(); + QStringList l; + l.reserve(num_slots); + + for (int i = 0; i < num_slots; ++i) { + if (manager()->bank->isUsed(i)) { + QUuid uuid = manager()->bank->appAt(i); + l.append(uuid.toString()); + } else { + l.append(QString()); + } + } + + Q_ASSERT(l.size() == num_slots); + + return l; +} + +QVariantList PebbledProxy::AllApps() const +{ + QList<QUuid> uuids = manager()->apps->appUuids(); + QVariantList l; + + foreach (const QUuid &uuid, uuids) { + const AppInfo &info = manager()->apps->info(uuid); + QVariantMap m; + m.insert("uuid", QVariant::fromValue(uuid.toString())); + m.insert("short-name", QVariant::fromValue(info.shortName())); + m.insert("long-name", QVariant::fromValue(info.longName())); + 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)); + } + + return l; +} + +bool PebbledProxy::SendAppMessage(const QString &uuid, const QVariantMap &data) +{ + Q_ASSERT(calledFromDBus()); + const QDBusMessage msg = message(); + setDelayedReply(true); + manager()->appmsg->send(uuid, data, [this, msg]() { + QDBusMessage reply = msg.createReply(QVariant::fromValue(true)); + this->connection().send(reply); + }, [this, msg]() { + QDBusMessage reply = msg.createReply(QVariant::fromValue(false)); + this->connection().send(reply); + }); + return false; // D-Bus clients should never see this reply. +} + +QString PebbledProxy::StartAppConfiguration(const QString &uuid) +{ + Q_ASSERT(calledFromDBus()); + const QDBusMessage msg = message(); + QDBusConnection conn = connection(); + + if (manager()->currentAppUuid != uuid) { + qCWarning(l) << "Called StartAppConfiguration but the uuid" << uuid << "is not running"; + sendErrorReply(msg.interface() + ".Error.AppNotRunning", + "The requested app is not currently opened in the watch"); + return QString(); + } + + if (!manager()->js->isJSKitAppRunning()) { + qCWarning(l) << "Called StartAppConfiguration but the uuid" << uuid << "is not a JS app"; + sendErrorReply(msg.interface() + ".Error.JSNotActive", + "The requested app is not a PebbleKit JS application"); + return QString(); + } + + // After calling showConfiguration() on the script, + // it will (eventually!) return a URL to us via the appOpenUrl signal. + + // So we can't send the D-Bus reply right now. + setDelayedReply(true); + + // Set up a signal handler to catch the appOpenUrl signal. + QMetaObject::Connection *c = new QMetaObject::Connection; + *c = connect(manager()->js, &JSKitManager::appOpenUrl, + this, [this,conn,msg,c](const QUrl &url) { + // Workaround: due to a GCC crash we can't capture the uuid parameter, but we can extract + // it again from the original message arguments. + // Suspect GCC bug# is 59195, 61233, or 61321. + // TODO Possibly fixed in 4.9.0 + const QString uuid = msg.arguments().at(0).toString(); + + if (manager()->currentAppUuid != uuid) { + // App was changed while we were waiting for the script.. + QDBusMessage reply = msg.createErrorReply(msg.interface() + ".Error.AppNotRunning", + "The requested app is not currently opened in the watch"); + conn.send(reply); + } else { + QDBusMessage reply = msg.createReply(QVariant::fromValue(url.toString())); + conn.send(reply); + } + + disconnect(*c); + delete c; + }); + + // TODO: JS script may fail, never call OpenURL, or something like that + // In those cases we WILL leak the above connection. + // (at least until the next appOpenURL event comes in) + // So we need to also set a timeout or similar. + + manager()->js->showConfiguration(); + + // Note that the above signal handler _might_ have been already called by this point. + + return QString(); // This return value should never be used. +} + +void PebbledProxy::SendAppConfigurationData(const QString &uuid, const QString &data) +{ + Q_ASSERT(calledFromDBus()); + const QDBusMessage msg = message(); + + if (manager()->currentAppUuid != uuid) { + sendErrorReply(msg.interface() + ".Error.AppNotRunning", + "The requested app is not currently opened in the watch"); + return; + } + + if (!manager()->js->isJSKitAppRunning()) { + sendErrorReply(msg.interface() + ".Error.JSNotActive", + "The requested app is not a PebbleKit JS application"); + return; + } + + manager()->js->handleWebviewClosed(data); +} + +void PebbledProxy::UnloadApp(int slot) +{ + Q_ASSERT(calledFromDBus()); + const QDBusMessage msg = message(); + + if (!manager()->bank->unloadApp(slot)) { + sendErrorReply(msg.interface() + ".Error.CannotUnload", + "Cannot unload application"); + } +} + +void PebbledProxy::UploadApp(const QString &uuid, int slot) +{ + Q_ASSERT(calledFromDBus()); + const QDBusMessage msg = message(); + + if (!manager()->bank->uploadApp(QUuid(uuid), slot)) { + sendErrorReply(msg.interface() + ".Error.CannotUpload", + "Cannot upload application"); + } +} diff --git a/daemon/manager.h b/daemon/manager.h index 4a3e760..9a4ed0f 100644 --- a/daemon/manager.h +++ b/daemon/manager.h @@ -3,9 +3,15 @@ #include "watchconnector.h" #include "dbusconnector.h" +#include "uploadmanager.h" #include "voicecallmanager.h" #include "notificationmanager.h" -#include "watchcommands.h" +#include "musicmanager.h" +#include "datalogmanager.h" +#include "appmsgmanager.h" +#include "jskitmanager.h" +#include "appmanager.h" +#include "bankmanager.h" #include "settings.h" #include <QObject> @@ -20,28 +26,32 @@ using namespace QtContacts; -class Manager : - public QObject, - protected QDBusContext +class PebbledProxy; + +class Manager : public QObject, protected QDBusContext { Q_OBJECT QLoggingCategory l; friend class PebbledProxy; - Q_PROPERTY(QString mpris READ mpris) - Q_PROPERTY(QVariantMap mprisMetadata READ getMprisMetadata WRITE setMprisMetadata NOTIFY mprisMetadataChanged) - QBluetoothLocalDevice btDevice; - watch::WatchConnector *watch; + Settings *settings; + + PebbledProxy *proxy; + + WatchConnector *watch; DBusConnector *dbus; + UploadManager *upload; + AppManager *apps; + BankManager *bank; VoiceCallManager *voice; NotificationManager *notifications; - - WatchCommands *commands; - - Settings *settings; + MusicManager *music; + DataLogManager *datalog; + AppMsgManager *appmsg; + JSKitManager *js; MNotification notification; @@ -50,30 +60,24 @@ class Manager : QString defaultProfile; - QString lastSeenMpris; + QUuid currentAppUuid; QScopedPointer<icu::Transliterator> transliterator; public: - explicit Manager(watch::WatchConnector *watch, DBusConnector *dbus, VoiceCallManager *voice, NotificationManager *notifications, Settings *settings); + explicit Manager(Settings *settings, QObject *parent = 0); + ~Manager(); - Q_INVOKABLE QString findPersonByNumber(QString number); - Q_INVOKABLE QString getCurrentProfile(); - Q_INVOKABLE QString mpris(); - QVariantMap mprisMetadata; - QVariantMap getMprisMetadata() { return mprisMetadata; } + QString findPersonByNumber(QString number); + QString getCurrentProfile() const; protected: void transliterateMessage(const QString &text); -signals: - void mprisMetadataChanged(QVariantMap); - public slots: - void hangupAll(); void applyProfile(); -protected slots: +private slots: void onSettingChanged(const QString &key); void onSettingsChanged(); void onPebbleChanged(); @@ -86,33 +90,69 @@ protected slots: void onTwitterNotify(const QString &sender, const QString &data); void onFacebookNotify(const QString &sender, const QString &data); void onEmailNotify(const QString &sender, const QString &data,const QString &subject); - void onMprisPropertiesChanged(QString,QMap<QString,QVariant>,QStringList); - void setMprisMetadata(QDBusArgument metadata); - void setMprisMetadata(QVariantMap metadata); + + void onAppNotification(const QUuid &uuid, const QString &title, const QString &body); + void onAppOpened(const QUuid &uuid); + void onAppClosed(const QUuid &uuid); }; -class PebbledProxy : public QObject +/** This class is what's actually exported over D-Bus, + * so the names of the slots and properties must match with org.pebbled.Watch D-Bus interface. + * Case sensitive. Otherwise, _runtime_ failures will occur. */ +// Some of the methods are marked inline so that they may be inlined inside qt_metacall +class PebbledProxy : public QObject, protected QDBusContext { Q_OBJECT - Q_PROPERTY(QVariantMap pebble READ pebble) - Q_PROPERTY(QString name READ pebbleName) - Q_PROPERTY(QString address READ pebbleAddress) - Q_PROPERTY(bool connected READ pebbleConnected) + QLoggingCategory l; + + Q_PROPERTY(QString Name READ Name NOTIFY NameChanged) + Q_PROPERTY(QString Address READ Address NOTIFY AddressChanged) + Q_PROPERTY(bool Connected READ Connected NOTIFY ConnectedChanged) + Q_PROPERTY(QString AppUuid READ AppUuid NOTIFY AppUuidChanged) + Q_PROPERTY(QStringList AppSlots READ AppSlots NOTIFY AppSlotsChanged) + Q_PROPERTY(QVariantList AllApps READ AllApps NOTIFY AllAppsChanged) - QVariantMap pebble() { return static_cast<Manager*>(parent())->dbus->pebble(); } - QString pebbleName() { return static_cast<Manager*>(parent())->dbus->pebble()["Name"].toString(); } - QString pebbleAddress() { return static_cast<Manager*>(parent())->dbus->pebble()["Address"].toString(); } - bool pebbleConnected() { return static_cast<Manager*>(parent())->watch->isConnected(); } + inline Manager* manager() const { return static_cast<Manager*>(parent()); } + inline QVariantMap pebble() const { return manager()->dbus->pebble(); } public: - explicit PebbledProxy(QObject *parent) : QObject(parent) {} + inline explicit PebbledProxy(QObject *parent) + : QObject(parent), l(metaObject()->className()) {} + + inline QString Name() const { return pebble()["Name"].toString(); } + inline QString Address() const { return pebble()["Address"].toString(); } + inline bool Connected() const { return manager()->watch->isConnected(); } + inline QString AppUuid() const { return manager()->currentAppUuid.toString(); } + + QStringList AppSlots() const; + + QVariantList AllApps() const; public slots: - void ping(int val) { static_cast<Manager*>(parent())->watch->ping((unsigned int)val); } - void time() { static_cast<Manager*>(parent())->watch->time(); } - void disconnect() { static_cast<Manager*>(parent())->watch->disconnect(); } - void reconnect() { static_cast<Manager*>(parent())->watch->reconnect(); } + inline void Disconnect() { manager()->watch->disconnect(); } + inline void Reconnect() { manager()->watch->reconnect(); } + inline void Ping(uint val) { manager()->watch->ping(val); } + inline void SyncTime() { manager()->watch->time(); } + + inline void LaunchApp(const QString &uuid) { manager()->appmsg->launchApp(uuid); } + inline void CloseApp(const QString &uuid) { manager()->appmsg->closeApp(uuid); } + + bool SendAppMessage(const QString &uuid, const QVariantMap &data); + QString StartAppConfiguration(const QString &uuid); + void SendAppConfigurationData(const QString &uuid, const QString &data); + void UnloadApp(int slot); + void UploadApp(const QString &uuid, int slot); + +signals: + void NameChanged(); + void AddressChanged(); + void ConnectedChanged(); + void AppUuidChanged(); + void AppSlotsChanged(); + void AllAppsChanged(); + void AppOpened(const QString &uuid); + void AppClosed(const QString &uuid); }; #endif // MANAGER_H diff --git a/daemon/musicmanager.cpp b/daemon/musicmanager.cpp new file mode 100644 index 0000000..385abbf --- /dev/null +++ b/daemon/musicmanager.cpp @@ -0,0 +1,221 @@ +#include <QDBusConnection> +#include <QDBusConnectionInterface> +#include "musicmanager.h" + +MusicManager::MusicManager(WatchConnector *watch, QObject *parent) + : QObject(parent), l(metaObject()->className()), + watch(watch), _watcher(new QDBusServiceWatcher(this)) +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + QDBusConnectionInterface *bus_iface = bus.interface(); + + // This watcher will be used to find when the current MPRIS service dies + // (and thus we must clear the metadata) + _watcher->setConnection(bus); + connect(_watcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &MusicManager::handleMprisServiceOwnerChanged); + + // Try to find an active MPRIS service to initially connect to + const QStringList &services = bus_iface->registeredServiceNames(); + foreach (QString service, services) { + if (service.startsWith("org.mpris.MediaPlayer2.")) { + switchToService(service); + fetchMetadataFromService(); + // The watch is not connected by this point, + // so we don't send the current metadata. + break; + } + } + + // Even if we didn't find any service, we still listen for metadataChanged signals + // from every MPRIS-compatible player + // If such a signal comes in, we will connect to the source service for that signal + bus.connect("", "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", + this, SLOT(handleMprisPropertiesChanged(QString,QMap<QString,QVariant>,QStringList))); + + // Now set up the Pebble endpoint handler for music control commands + watch->setEndpointHandler(WatchConnector::watchMUSIC_CONTROL, [this](const QByteArray& data) { + handleMusicControl(WatchConnector::MusicControl(data.at(0))); + return true; + }); + + // If the watch disconnects, we will send the current metadata when it comes back. + connect(watch, &WatchConnector::connectedChanged, + this, &MusicManager::handleWatchConnected); +} + +void MusicManager::switchToService(const QString &service) +{ + if (_curService != service) { + qCDebug(l) << "switching to mpris service" << service; + _curService = service; + + if (_curService.isEmpty()) { + _watcher->setWatchedServices(QStringList()); + } else { + _watcher->setWatchedServices(QStringList(_curService)); + } + } +} + +void MusicManager::fetchMetadataFromService() +{ + _curMetadata.clear(); + + if (!_curService.isEmpty()) { + QDBusMessage call = QDBusMessage::createMethodCall(_curService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); + call << "org.mpris.MediaPlayer2.Player" << "Metadata"; + QDBusReply<QDBusVariant> reply = QDBusConnection::sessionBus().call(call); + if (reply.isValid()) { + qCDebug(l) << "got mpris metadata from service" << _curService; + _curMetadata = qdbus_cast<QVariantMap>(reply.value().variant().value<QDBusArgument>()); + } else { + qCWarning(l) << reply.error().message(); + } + } +} + +void MusicManager::sendCurrentMprisMetadata() +{ + Q_ASSERT(watch->isConnected()); + + QString track = _curMetadata.value("xesam:title").toString().left(30); + QString album = _curMetadata.value("xesam:album").toString().left(30); + QString artist = _curMetadata.value("xesam:artist").toString().left(30); + + qCDebug(l) << "sending mpris metadata:" << track << album << artist; + + watch->sendMusicNowPlaying(track, album, artist); +} + +void MusicManager::callMprisMethod(const QString &method) +{ + Q_ASSERT(!method.isEmpty()); + Q_ASSERT(!_curService.isEmpty()); + + qCDebug(l) << _curService << "->" << method; + + QDBusConnection bus = QDBusConnection::sessionBus(); + QDBusMessage call = QDBusMessage::createMethodCall(_curService, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", + method); + + QDBusError err = bus.call(call); + + if (err.isValid()) { + qCWarning(l) << "while calling mpris method on" << _curService << ":" << err.message(); + } +} + +void MusicManager::handleMusicControl(WatchConnector::MusicControl operation) +{ + qCDebug(l) << "operation from watch:" << operation; + + if (_curService.isEmpty()) { + qCDebug(l) << "can't do any music operation, no mpris interface active"; + return; + } + + switch (operation) { + case WatchConnector::musicPLAY_PAUSE: + callMprisMethod("PlayPause"); + break; + case WatchConnector::musicPAUSE: + callMprisMethod("Pause"); + break; + case WatchConnector::musicPLAY: + callMprisMethod("Play"); + break; + case WatchConnector::musicNEXT: + callMprisMethod("Next"); + break; + case WatchConnector::musicPREVIOUS: + callMprisMethod("Previous"); + break; + + case WatchConnector::musicVOLUME_UP: + case WatchConnector::musicVOLUME_DOWN: { + QDBusConnection bus = QDBusConnection::sessionBus(); + QDBusMessage call = QDBusMessage::createMethodCall(_curService, "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", "Get"); + call << "org.mpris.MediaPlayer2.Player" << "Volume"; + QDBusReply<QDBusVariant> volumeReply = bus.call(call); + if (volumeReply.isValid()) { + double volume = volumeReply.value().variant().toDouble(); + if (operation == WatchConnector::musicVOLUME_UP) { + volume += 0.1; + } + else { + volume -= 0.1; + } + qCDebug(l) << "Setting volume" << volume; + + call = QDBusMessage::createMethodCall(_curService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Set"); + call << "org.mpris.MediaPlayer2.Player" << "Volume" << QVariant::fromValue(QDBusVariant(volume)); + + QDBusError err = QDBusConnection::sessionBus().call(call); + if (err.isValid()) { + qCWarning(l) << err.message(); + } + } else { + qCWarning(l) << volumeReply.error().message(); + } + } + break; + + case WatchConnector::musicGET_NOW_PLAYING: + sendCurrentMprisMetadata(); + break; + + default: + qCWarning(l) << "Operation" << operation << "not supported"; + break; + } +} + +void MusicManager::handleMprisServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(oldOwner); + if (name == _curService && newOwner.isEmpty()) { + // Oops, current service is going away + switchToService(QString()); + _curMetadata.clear(); + if (watch->isConnected()) { + sendCurrentMprisMetadata(); + } + } +} + +void MusicManager::handleMprisPropertiesChanged(const QString &interface, const QMap<QString, QVariant> &changed, const QStringList &invalidated) +{ + Q_ASSERT(calledFromDBus()); + Q_UNUSED(interface); + Q_UNUSED(invalidated); + + if (changed.contains("Metadata")) { + QVariantMap metadata = qdbus_cast<QVariantMap>(changed.value("Metadata").value<QDBusArgument>()); + qCDebug(l) << "received new metadata" << metadata; + _curMetadata = metadata; + } + + if (changed.contains("PlaybackStatus")) { + QString status = changed.value("PlaybackStatus").toString(); + if (status == "Stopped") { + _curMetadata.clear(); + } + } + + if (watch->isConnected()) { + sendCurrentMprisMetadata(); + } + + switchToService(message().service()); +} + +void MusicManager::handleWatchConnected() +{ + if (watch->isConnected()) { + sendCurrentMprisMetadata(); + } +} diff --git a/daemon/musicmanager.h b/daemon/musicmanager.h new file mode 100644 index 0000000..14aa6fb --- /dev/null +++ b/daemon/musicmanager.h @@ -0,0 +1,36 @@ +#ifndef MUSICMANAGER_H +#define MUSICMANAGER_H + +#include <QObject> +#include <QDBusContext> +#include <QDBusServiceWatcher> +#include "watchconnector.h" + +class MusicManager : public QObject, protected QDBusContext +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit MusicManager(WatchConnector *watch, QObject *parent = 0); + +private: + void switchToService(const QString &service); + void fetchMetadataFromService(); + void sendCurrentMprisMetadata(); + void callMprisMethod(const QString &method); + +private slots: + void handleMusicControl(WatchConnector::MusicControl operation); + void handleMprisServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner); + void handleMprisPropertiesChanged(const QString &interface, const QMap<QString,QVariant> &changed, const QStringList &invalidated); + void handleWatchConnected(); + +private: + WatchConnector *watch; + QDBusServiceWatcher *_watcher; + QString _curService; + QVariantMap _curMetadata; +}; + +#endif // MUSICMANAGER_H diff --git a/daemon/org.pebbled.xml b/daemon/org.pebbled.xml deleted file mode 100644 index e255782..0000000 --- a/daemon/org.pebbled.xml +++ /dev/null @@ -1,20 +0,0 @@ -<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" -"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> -<node> - <interface name="org.pebbled"> - <property name="pebble" type="a{sv}" access="read"> - <annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantMap"/> - </property> - <property name="name" type="s" access="read"/> - <property name="address" type="s" access="read"/> - <property name="connected" type="b" access="read"/> - <signal name="pebbleChanged"/> - <signal name="connectedChanged"/> - <method name="ping"> - <arg name="val" type="i" direction="in"/> - </method> - <method name="time"/> - <method name="disconnect"/> - <method name="reconnect"/> - </interface> -</node> diff --git a/daemon/packer.cpp b/daemon/packer.cpp new file mode 100644 index 0000000..abbb873 --- /dev/null +++ b/daemon/packer.cpp @@ -0,0 +1,132 @@ +#include "packer.h" +#include "watchconnector.h" + +QLoggingCategory Packer::l("Packer"); + +void Packer::writeBytes(int n, const QByteArray &b) +{ + if (b.size() > n) { + _buf->append(b.constData(), n); + } else { + int diff = n - b.size(); + _buf->append(b); + if (diff > 0) { + _buf->append(QByteArray(diff, '\0')); + } + } +} + +void Packer::writeCString(const QString &s) +{ + _buf->append(s.toUtf8()); + _buf->append('\0'); +} + +void Packer::writeUuid(const QUuid &uuid) +{ + writeBytes(16, uuid.toRfc4122()); +} + +void Packer::writeDict(const QMap<int, QVariant> &d) +{ + int size = d.size(); + if (size > 0xFF) { + qCWarning(l) << "Dictionary is too large to encode"; + writeLE<quint8>(0); + return; + } + + writeLE<quint8>(size); + + for (QMap<int, QVariant>::const_iterator it = d.constBegin(); it != d.constEnd(); ++it) { + writeLE<quint32>(it.key()); + + switch (int(it.value().type())) { + case QMetaType::Char: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(char)); + writeLE<char>(it.value().value<char>()); + break; + case QMetaType::Short: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(short)); + writeLE<short>(it.value().value<short>()); + break; + case QMetaType::Int: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(int)); + writeLE<int>(it.value().value<int>()); + break; + + case QMetaType::UChar: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(char)); + writeLE<char>(it.value().value<char>()); + break; + case QMetaType::UShort: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(short)); + writeLE<short>(it.value().value<short>()); + break; + case QMetaType::UInt: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(int)); + writeLE<int>(it.value().value<int>()); + break; + + case QMetaType::Bool: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(char)); + writeLE<char>(it.value().value<char>()); + break; + + case QMetaType::Float: // Treat qreals as ints + case QMetaType::Double: + writeLE<quint8>(WatchConnector::typeINT); + writeLE<quint16>(sizeof(int)); + writeLE<int>(it.value().value<int>()); + break; + + case QMetaType::QByteArray: { + QByteArray ba = it.value().toByteArray(); + writeLE<quint8>(WatchConnector::typeBYTES); + writeLE<quint16>(ba.size()); + _buf->append(ba); + break; + } + + case QMetaType::QVariantList: { + // Generally a JS array, which we marshal as a byte array. + QVariantList list = it.value().toList(); + QByteArray ba; + ba.reserve(list.size()); + + Q_FOREACH (const QVariant &v, list) { + ba.append(v.toInt()); + } + + writeLE<quint8>(WatchConnector::typeBYTES); + writeLE<quint16>(ba.size()); + _buf->append(ba); + break; + } + + default: + qCWarning(l) << "Unknown dict item type:" << it.value().typeName(); + /* Fallthrough */ + case QMetaType::QString: + case QMetaType::QUrl: + { + QByteArray s = it.value().toString().toUtf8(); + if (s.isEmpty() || s[s.size() - 1] != '\0') { + // Add null terminator if it doesn't have one + s.append('\0'); + } + writeLE<quint8>(WatchConnector::typeSTRING); + writeLE<quint16>(s.size()); + _buf->append(s); + break; + } + } + } +} diff --git a/daemon/packer.h b/daemon/packer.h new file mode 100644 index 0000000..fbf5f4b --- /dev/null +++ b/daemon/packer.h @@ -0,0 +1,69 @@ +#ifndef PACKER_H +#define PACKER_H + +#include <QtEndian> +#include <QByteArray> +#include <QString> +#include <QUuid> +#include <QVariantMap> +#include <QLoggingCategory> + +class Packer +{ + static QLoggingCategory l; + +public: + Packer(QByteArray *buf); + + template <typename T> + void write(T v); + + template <typename T> + void writeLE(T v); + + void writeBytes(int n, const QByteArray &b); + + void writeFixedString(int n, const QString &s); + + void writeCString(const QString &s); + + void writeUuid(const QUuid &uuid); + + void writeDict(const QMap<int, QVariant> &d); + +private: + char *p(int n); + uchar *up(int n); + QByteArray *_buf; +}; + +inline Packer::Packer(QByteArray *buf) + : _buf(buf) +{ +} + +template <typename T> +void Packer::write(T v) +{ + qToBigEndian(v, up(sizeof(T))); +} + +template <typename T> +void Packer::writeLE(T v) +{ + qToLittleEndian(v, up(sizeof(T))); +} + +inline char * Packer::p(int n) +{ + int size = _buf->size(); + _buf->resize(size + n); + return &_buf->data()[size]; +} + +inline uchar * Packer::up(int n) +{ + return reinterpret_cast<uchar *>(p(n)); +} + +#endif // PACKER_H diff --git a/daemon/settings.h b/daemon/settings.h index d6db9b6..90e25e2 100644 --- a/daemon/settings.h +++ b/daemon/settings.h @@ -18,6 +18,8 @@ class Settings : public MDConfGroup Q_PROPERTY(bool notificationsFacebook MEMBER notificationsFacebook NOTIFY notificationsFacebookChanged) Q_PROPERTY(bool notificationsOther MEMBER notificationsOther NOTIFY notificationsOtherChanged) Q_PROPERTY(bool notificationsAll MEMBER notificationsAll NOTIFY notificationsAllChanged) + Q_PROPERTY(QString accountToken MEMBER accountToken NOTIFY accountTokenChanged) + bool silentWhenConnected; bool transliterateMessage; bool incomingCallNotification; @@ -29,6 +31,7 @@ class Settings : public MDConfGroup bool notificationsFacebook; bool notificationsOther; bool notificationsAll; + QString accountToken; public: explicit Settings(QObject *parent = 0) : @@ -36,20 +39,18 @@ public: { resolveMetaObject(); } signals: - void silentWhenConnectedChanged(bool); - void transliterateMessageChanged(bool); - void incomingCallNotificationChanged(bool); - void notificationsCommhistorydChanged(bool); - void notificationsMissedCallChanged(bool); - void notificationsEmailsChanged(bool); - void notificationsMitakuuluuChanged(bool); - void notificationsTwitterChanged(bool); - void notificationsFacebookChanged(bool); - void notificationsOtherChanged(bool); - void notificationsAllChanged(bool); - -public slots: - + void silentWhenConnectedChanged(); + void transliterateMessageChanged(); + void incomingCallNotificationChanged(); + void notificationsCommhistorydChanged(); + void notificationsMissedCallChanged(); + void notificationsEmailsChanged(); + void notificationsMitakuuluuChanged(); + void notificationsTwitterChanged(); + void notificationsFacebookChanged(); + void notificationsOtherChanged(); + void notificationsAllChanged(); + void accountTokenChanged(); }; #endif // SETTINGS_H diff --git a/daemon/stm32crc.cpp b/daemon/stm32crc.cpp new file mode 100644 index 0000000..d4e5bd9 --- /dev/null +++ b/daemon/stm32crc.cpp @@ -0,0 +1,156 @@ +#include <QtEndian> +#include "stm32crc.h" + +namespace +{ + +/** Precomputed CRC polynomial + * Generated by pycrc v0.8.2, http://www.tty1.net/pycrc/ + * using the configuration: + * Width = 32 + * Poly = 0x04c11db7 + * XorIn = 0xffffffff + * ReflectIn = False + * XorOut = 0xffffffff + * ReflectOut = False + * Algorithm = table-driven + * The algorithm has been modified to use 32bit word size like STM32 + *****************************************************************************/ +const quint32 crc_table[256] = { + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, + 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, + 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, + 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, + 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, + 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, + 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, + 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, + 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, + 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, + 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, + 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, + 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, + 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, + 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, + 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, + 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, + 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, + 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, + 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, + 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, + 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 +}; + +const int word_len = sizeof(quint32); + +quint32 calc_crc(quint32 crc, quint32 word) +{ + crc ^= word; + crc = (crc << 8) ^ crc_table[(crc >> 24) & 0xFF]; + crc = (crc << 8) ^ crc_table[(crc >> 24) & 0xFF]; + crc = (crc << 8) ^ crc_table[(crc >> 24) & 0xFF]; + crc = (crc << 8) ^ crc_table[(crc >> 24) & 0xFF]; + return crc; +} + +} + +Stm32Crc::Stm32Crc() +{ + reset(); +} + +void Stm32Crc::reset() +{ + crc = 0xFFFFFFFFU; + rem = 0; + memset(buffer, 0, word_len); +} + +void Stm32Crc::addData(const char *data, int length) +{ + int off = 0; + + if (rem > 0) { + for (; rem < word_len && off < length; ++rem, ++off) { + buffer[rem] = data[off]; + } + + Q_ASSERT(rem <= word_len); + + if (rem == word_len) { + crc = calc_crc(crc, qFromLittleEndian<quint32>(buffer)); + memset(buffer, 0, word_len); + rem = 0; + } + } + + Q_ASSERT(rem == 0 || off == length); + + for (; off < (length/word_len)*word_len; off+=word_len) { + quint32 word = qFromLittleEndian<quint32>(reinterpret_cast<const uchar*>(&data[off])); + crc = calc_crc(crc, word); + } + + for (; off < length; ++off, ++rem) { + buffer[rem] = data[off]; + } + + Q_ASSERT(rem <= word_len); +} + +void Stm32Crc::addData(const QByteArray &data) +{ + addData(data.constData(), data.length()); +} + +quint32 Stm32Crc::result() const +{ + if (rem > 0) { + return calc_crc(crc, qFromLittleEndian<quint32>(buffer)); + } else { + return crc; + } +} diff --git a/daemon/stm32crc.h b/daemon/stm32crc.h new file mode 100644 index 0000000..b21f5ed --- /dev/null +++ b/daemon/stm32crc.h @@ -0,0 +1,24 @@ +#ifndef STM32CRC_H +#define STM32CRC_H + +#include <QByteArray> + +class Stm32Crc +{ +public: + Stm32Crc(); + + void reset(); + + void addData(const char *data, int length); + void addData(const QByteArray &data); + + quint32 result() const; + +private: + quint32 crc; + quint8 buffer[4]; + quint8 rem; +}; + +#endif // STM32CRC_H diff --git a/daemon/unpacker.cpp b/daemon/unpacker.cpp new file mode 100644 index 0000000..1f1d564 --- /dev/null +++ b/daemon/unpacker.cpp @@ -0,0 +1,90 @@ +#include "unpacker.h" +#include "watchconnector.h" + +QLoggingCategory Unpacker::l("Unpacker"); + +QByteArray Unpacker::readBytes(int n) +{ + if (checkBad(n)) return QByteArray(); + const char *u = &_buf.constData()[_offset]; + _offset += n; + return QByteArray(u, n); +} + +QString Unpacker::readFixedString(int n) +{ + if (checkBad(n)) return QString(); + const char *u = &_buf.constData()[_offset]; + _offset += n; + return QString::fromUtf8(u, strnlen(u, n)); +} + +QUuid Unpacker::readUuid() +{ + if (checkBad(16)) return QString(); + _offset += 16; + return QUuid::fromRfc4122(_buf.mid(_offset - 16, 16)); +} + +QMap<int, QVariant> Unpacker::readDict() +{ + QMap<int, QVariant> d; + if (checkBad(1)) return d; + + const int n = readLE<quint8>(); + + for (int i = 0; i < n; i++) { + if (checkBad(4 + 1 + 2)) return d; + const int key = readLE<qint32>(); // For some reason, this is little endian. + const int type = readLE<quint8>(); + const int width = readLE<quint16>(); + + switch (type) { + case WatchConnector::typeBYTES: + d.insert(key, QVariant::fromValue(readBytes(width))); + break; + case WatchConnector::typeSTRING: + d.insert(key, QVariant::fromValue(readFixedString(width))); + break; + case WatchConnector::typeUINT: + switch (width) { + case sizeof(quint8): + d.insert(key, QVariant::fromValue(readLE<quint8>())); + break; + case sizeof(quint16): + d.insert(key, QVariant::fromValue(readLE<quint16>())); + break; + case sizeof(quint32): + d.insert(key, QVariant::fromValue(readLE<quint32>())); + break; + default: + _bad = true; + return d; + } + + break; + case WatchConnector::typeINT: + switch (width) { + case sizeof(qint8): + d.insert(key, QVariant::fromValue(readLE<qint8>())); + break; + case sizeof(qint16): + d.insert(key, QVariant::fromValue(readLE<qint16>())); + break; + case sizeof(qint32): + d.insert(key, QVariant::fromValue(readLE<qint32>())); + break; + default: + _bad = true; + return d; + } + + break; + default: + _bad = true; + return d; + } + } + + return d; +} diff --git a/daemon/unpacker.h b/daemon/unpacker.h new file mode 100644 index 0000000..46e6d57 --- /dev/null +++ b/daemon/unpacker.h @@ -0,0 +1,92 @@ +#ifndef UNPACKER_H +#define UNPACKER_H + +#include <QtEndian> +#include <QByteArray> +#include <QString> +#include <QUuid> +#include <QVariantMap> +#include <QLoggingCategory> + +class Unpacker +{ + static QLoggingCategory l; + +public: + Unpacker(const QByteArray &data); + + template <typename T> + T read(); + + template <typename T> + T readLE(); + + QByteArray readBytes(int n); + + QString readFixedString(int n); + + QUuid readUuid(); + + QMap<int, QVariant> readDict(); + + void skip(int n); + + bool bad() const; + +private: + const uchar * p(); + bool checkBad(int n = 0); + + const QByteArray &_buf; + int _offset; + bool _bad; +}; + +inline Unpacker::Unpacker(const QByteArray &data) + : _buf(data), _offset(0), _bad(false) +{ +} + +template <typename T> +inline T Unpacker::read() +{ + if (checkBad(sizeof(T))) return 0; + const uchar *u = p(); + _offset += sizeof(T); + return qFromBigEndian<T>(u); +} + +template <typename T> +inline T Unpacker::readLE() +{ + if (checkBad(sizeof(T))) return 0; + const uchar *u = p(); + _offset += sizeof(T); + return qFromLittleEndian<T>(u); +} + +inline void Unpacker::skip(int n) +{ + _offset += n; + checkBad(); +} + +inline bool Unpacker::bad() const +{ + return _bad; +} + +inline const uchar * Unpacker::p() +{ + return reinterpret_cast<const uchar *>(&_buf.constData()[_offset]); +} + +inline bool Unpacker::checkBad(int n) +{ + if (_offset + n > _buf.size()) { + _bad = true; + } + return _bad; +} + +#endif // UNPACKER_H diff --git a/daemon/uploadmanager.cpp b/daemon/uploadmanager.cpp new file mode 100644 index 0000000..b379880 --- /dev/null +++ b/daemon/uploadmanager.cpp @@ -0,0 +1,300 @@ +#include "uploadmanager.h" +#include "unpacker.h" +#include "packer.h" +#include "stm32crc.h" + +static const int CHUNK_SIZE = 2000; +using std::function; + +UploadManager::UploadManager(WatchConnector *watch, QObject *parent) : + QObject(parent), l(metaObject()->className()), watch(watch), + _lastUploadId(0), _state(StateNotStarted) +{ + watch->setEndpointHandler(WatchConnector::watchPUTBYTES, + [this](const QByteArray &msg) { + if (_pending.empty()) { + qCWarning(l) << "putbytes message, but queue is empty!"; + return false; + } + handleMessage(msg); + return true; + }); +} + +uint UploadManager::upload(WatchConnector::UploadType type, int index, const QString &filename, QIODevice *device, int size, + SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + PendingUpload upload; + upload.id = ++_lastUploadId; + upload.type = type; + upload.index = index; + upload.filename = filename; + upload.device = device; + if (size < 0) { + upload.size = device->size(); + } else { + upload.size = size; + } + upload.remaining = upload.size; + upload.successCallback = successCallback; + upload.errorCallback = errorCallback; + upload.progressCallback = progressCallback; + + if (upload.remaining <= 0) { + qCWarning(l) << "upload is empty"; + if (errorCallback) { + errorCallback(-1); + return -1; + } + } + + _pending.enqueue(upload); + + if (_pending.size() == 1) { + startNextUpload(); + } + + return upload.id; +} + +uint UploadManager::uploadAppBinary(int slot, QIODevice *device, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(WatchConnector::uploadBINARY, slot, QString(), device, -1, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadAppResources(int slot, QIODevice *device, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(WatchConnector::uploadRESOURCES, slot, QString(), device, -1, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadFile(const QString &filename, QIODevice *device, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + Q_ASSERT(!filename.isEmpty()); + return upload(WatchConnector::uploadFILE, 0, filename, device, -1, successCallback, errorCallback, progressCallback); +} + +void UploadManager::cancel(uint id, int code) +{ + if (_pending.empty()) { + qCWarning(l) << "cannot cancel, empty queue"; + return; + } + + if (id == _pending.head().id) { + PendingUpload upload = _pending.dequeue(); + qCDebug(l) << "aborting current upload" << id << "(code:" << code << ")"; + + if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) { + QByteArray msg; + Packer p(&msg); + p.write<quint8>(WatchConnector::putbytesABORT); + p.write<quint32>(_token); + + qCDebug(l) << "sending abort for upload" << id; + + watch->sendMessage(WatchConnector::watchPUTBYTES, msg); + } + + _state = StateNotStarted; + _token = 0; + + if (upload.errorCallback) { + upload.errorCallback(code); + } + + if (!_pending.empty()) { + startNextUpload(); + } + } else { + for (int i = 1; i < _pending.size(); ++i) { + if (_pending[i].id == id) { + qCDebug(l) << "cancelling upload" << id << "(code:" << code << ")"; + if (_pending[i].errorCallback) { + _pending[i].errorCallback(code); + } + _pending.removeAt(i); + return; + } + } + qCWarning(l) << "cannot cancel, id" << id << "not found"; + } +} + +void UploadManager::startNextUpload() +{ + Q_ASSERT(!_pending.empty()); + Q_ASSERT(_state == StateNotStarted); + + PendingUpload &upload = _pending.head(); + QByteArray msg; + Packer p(&msg); + p.write<quint8>(WatchConnector::putbytesINIT); + p.write<quint32>(upload.remaining); + p.write<quint8>(upload.type); + p.write<quint8>(upload.index); + if (!upload.filename.isEmpty()) { + p.writeCString(upload.filename); + } + + qCDebug(l) << "starting new upload, size:" << upload.remaining << ", type:" << upload.type << ", slot:" << upload.index; + + _state = StateWaitForToken; + watch->sendMessage(WatchConnector::watchPUTBYTES, msg); +} + +void UploadManager::handleMessage(const QByteArray &msg) +{ + Q_ASSERT(!_pending.empty()); + PendingUpload &upload = _pending.head(); + + Unpacker u(msg); + int status = u.read<quint8>(); + + if (u.bad() || status != 1) { + qCWarning(l) << "upload" << upload.id << "got error code=" << status; + cancel(upload.id, status); + return; + } + + quint32 recv_token = u.read<quint32>(); + + if (u.bad()) { + qCWarning(l) << "upload" << upload.id << ": could not read the token"; + cancel(upload.id, -1); + return; + } + + if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) { + if (recv_token != _token) { + qCWarning(l) << "upload" << upload.id << ": invalid token"; + cancel(upload.id, -1); + return; + } + } + + switch (_state) { + case StateNotStarted: + qCWarning(l) << "got packet when upload is not started"; + break; + case StateWaitForToken: + qCDebug(l) << "token received"; + _token = recv_token; + _state = StateInProgress; + + /* fallthrough */ + case StateInProgress: + qCDebug(l) << "moving to the next chunk"; + if (upload.progressCallback) { + // Report that the previous chunk has been succesfully uploaded + upload.progressCallback(1.0 - (qreal(upload.remaining) / upload.size)); + } + if (upload.remaining > 0) { + if (!uploadNextChunk(upload)) { + cancel(upload.id, -1); + return; + } + } else { + qCDebug(l) << "no additional chunks, commit"; + _state = StateCommit; + if (!commit(upload)) { + cancel(upload.id, -1); + return; + } + } + break; + case StateCommit: + qCDebug(l) << "commited succesfully"; + if (upload.progressCallback) { + // Report that all chunks have been succesfully uploaded + upload.progressCallback(1.0); + } + _state = StateComplete; + if (!complete(upload)) { + cancel(upload.id, -1); + return; + } + break; + case StateComplete: + qCDebug(l) << "upload" << upload.id << "succesful, invoking callback"; + if (upload.successCallback) { + upload.successCallback(); + } + _pending.dequeue(); + _token = 0; + _state = StateNotStarted; + if (!_pending.empty()) { + startNextUpload(); + } + break; + default: + qCWarning(l) << "received message in wrong state"; + break; + } +} + +bool UploadManager::uploadNextChunk(PendingUpload &upload) +{ + QByteArray chunk = upload.device->read(qMin<int>(upload.remaining, CHUNK_SIZE)); + + if (upload.remaining < CHUNK_SIZE && chunk.size() < upload.remaining) { + // Short read! + qCWarning(l) << "short read during upload" << upload.id; + return false; + } + + Q_ASSERT(!chunk.isEmpty()); + Q_ASSERT(_state = StateInProgress); + + QByteArray msg; + Packer p(&msg); + p.write<quint8>(WatchConnector::putbytesSEND); + p.write<quint32>(_token); + p.write<quint32>(chunk.size()); + msg.append(chunk); + + qCDebug(l) << "sending a chunk of" << chunk.size() << "bytes"; + + watch->sendMessage(WatchConnector::watchPUTBYTES, msg); + + upload.remaining -= chunk.size(); + upload.crc.addData(chunk); + + qCDebug(l) << "remaining" << upload.remaining << "/" << upload.size << "bytes"; + + return true; +} + +bool UploadManager::commit(PendingUpload &upload) +{ + Q_ASSERT(_state == StateCommit); + Q_ASSERT(upload.remaining == 0); + + QByteArray msg; + Packer p(&msg); + p.write<quint8>(WatchConnector::putbytesCOMMIT); + p.write<quint32>(_token); + p.write<quint32>(upload.crc.result()); + + qCDebug(l) << "commiting upload" << upload.id + << "with crc" << qPrintable(QString("0x%1").arg(upload.crc.result(), 0, 16)); + + watch->sendMessage(WatchConnector::watchPUTBYTES, msg); + + return true; +} + +bool UploadManager::complete(PendingUpload &upload) +{ + Q_ASSERT(_state == StateComplete); + + QByteArray msg; + Packer p(&msg); + p.write<quint8>(WatchConnector::putbytesCOMPLETE); + p.write<quint32>(_token); + + qCDebug(l) << "completing upload" << upload.id; + + watch->sendMessage(WatchConnector::watchPUTBYTES, msg); + + return true; +} diff --git a/daemon/uploadmanager.h b/daemon/uploadmanager.h new file mode 100644 index 0000000..1980f96 --- /dev/null +++ b/daemon/uploadmanager.h @@ -0,0 +1,74 @@ +#ifndef UPLOADMANAGER_H +#define UPLOADMANAGER_H + +#include <functional> +#include <QQueue> +#include "watchconnector.h" +#include "stm32crc.h" + +class UploadManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit UploadManager(WatchConnector *watch, QObject *parent = 0); + + typedef std::function<void()> SuccessCallback; + typedef std::function<void(int)> ErrorCallback; + typedef std::function<void(qreal)> ProgressCallback; + + uint upload(WatchConnector::UploadType type, int index, const QString &filename, QIODevice *device, int size = -1, + SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + + uint uploadAppBinary(int slot, QIODevice *device, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + uint uploadAppResources(int slot, QIODevice *device, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + uint uploadFile(const QString &filename, QIODevice *device, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + + void cancel(uint id, int code = 0); + +signals: + +public slots: + + +private: + enum State { + StateNotStarted, + StateWaitForToken, + StateInProgress, + StateCommit, + StateComplete + }; + + struct PendingUpload { + uint id; + + WatchConnector::UploadType type; + int index; + QString filename; + QIODevice *device; + int size; + int remaining; + Stm32Crc crc; + + SuccessCallback successCallback; + ErrorCallback errorCallback; + ProgressCallback progressCallback; + }; + + void startNextUpload(); + void handleMessage(const QByteArray &msg); + bool uploadNextChunk(PendingUpload &upload); + bool commit(PendingUpload &upload); + bool complete(PendingUpload &upload); + +private: + WatchConnector *watch; + QQueue<PendingUpload> _pending; + uint _lastUploadId; + State _state; + quint32 _token; +}; + +#endif // UPLOADMANAGER_H diff --git a/daemon/voicecallmanager.cpp b/daemon/voicecallmanager.cpp index 68f36e0..afb3629 100644 --- a/daemon/voicecallmanager.cpp +++ b/daemon/voicecallmanager.cpp @@ -148,6 +148,13 @@ void VoiceCallManager::dial(const QString &provider, const QString &msisdn) QObject::connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), SLOT(onPendingCallFinished(QDBusPendingCallWatcher*))); } +void VoiceCallManager::hangupAll() +{ + foreach (VoiceCallHandler* handler, voiceCalls()) { + handler->hangup(); + } +} + void VoiceCallManager::silenceRingtone() { Q_D(const VoiceCallManager); diff --git a/daemon/voicecallmanager.h b/daemon/voicecallmanager.h index e0b610a..ec51230 100644 --- a/daemon/voicecallmanager.h +++ b/daemon/voicecallmanager.h @@ -80,6 +80,7 @@ Q_SIGNALS: public Q_SLOTS: void dial(const QString &providerId, const QString &msisdn); + void hangupAll(); void silenceRingtone(); diff --git a/daemon/watchcommands.cpp b/daemon/watchcommands.cpp deleted file mode 100644 index 9845849..0000000 --- a/daemon/watchcommands.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "watchcommands.h" - -#include <QDBusConnection> -#include <QDBusMessage> -#include <QDBusReply> - -using namespace watch; - -WatchCommands::WatchCommands(WatchConnector *watch, QObject *parent) : - QObject(parent), l(metaObject()->className()), watch(watch) -{} - -void WatchCommands::processMessage(uint endpoint, QByteArray data) -{ - qCDebug(l) << __FUNCTION__ << endpoint << "/" << data.toHex() << data.length(); - switch (endpoint) { - case WatchConnector::watchPHONE_VERSION: - watch->sendPhoneVersion(); - break; - case WatchConnector::watchPHONE_CONTROL: - if (data.at(0) == WatchConnector::callHANGUP) { - emit hangup(); - } - break; - case WatchConnector::watchMUSIC_CONTROL: - musicControl(WatchConnector::MusicControl(data.at(0))); - break; - case WatchConnector::watchSYSTEM_MESSAGE: - qCDebug(l) << "Got SYSTEM_MESSAGE" << WatchConnector::SystemMessage(data.at(0)); - // TODO: handle systemBLUETOOTH_START_DISCOVERABLE/systemBLUETOOTH_END_DISCOVERABLE - break; - - default: - qCDebug(l) << __FUNCTION__ << "endpoint" << endpoint << "not supported yet"; - } -} - -void WatchCommands::onMprisMetadataChanged(QVariantMap metadata) -{ - QString track = metadata.value("xesam:title").toString(); - QString album = metadata.value("xesam:album").toString(); - QString artist = metadata.value("xesam:artist").toString(); - qCDebug(l) << __FUNCTION__ << track << album << artist; - watch->sendMusicNowPlaying(track, album, artist); -} - -void WatchCommands::musicControl(WatchConnector::MusicControl operation) -{ - qCDebug(l) << "Operation:" << operation; - - QString mpris = parent()->property("mpris").toString(); - if (mpris.isEmpty()) { - qCDebug(l) << "No mpris interface active"; - return; - } - - QString method; - - switch(operation) { - case WatchConnector::musicPLAY_PAUSE: - method = "PlayPause"; - break; - case WatchConnector::musicPAUSE: - method = "Pause"; - break; - case WatchConnector::musicPLAY: - method = "Play"; - break; - case WatchConnector::musicNEXT: - method = "Next"; - break; - case WatchConnector::musicPREVIOUS: - method = "Previous"; - break; - case WatchConnector::musicVOLUME_UP: - case WatchConnector::musicVOLUME_DOWN: { - QDBusReply<QDBusVariant> VolumeReply = QDBusConnection::sessionBus().call( - QDBusMessage::createMethodCall(mpris, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get") - << "org.mpris.MediaPlayer2.Player" << "Volume"); - if (VolumeReply.isValid()) { - double volume = VolumeReply.value().variant().toDouble(); - if (operation == WatchConnector::musicVOLUME_UP) { - volume += 0.1; - } - else { - volume -= 0.1; - } - qCDebug(l) << "Setting volume" << volume; - QDBusError err = QDBusConnection::sessionBus().call( - QDBusMessage::createMethodCall(mpris, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Set") - << "org.mpris.MediaPlayer2.Player" << "Volume" << QVariant::fromValue(QDBusVariant(volume))); - if (err.isValid()) { - qCCritical(l) << err.message(); - } - } - else { - qCCritical(l) << VolumeReply.error().message(); - } - } - return; - case WatchConnector::musicGET_NOW_PLAYING: - onMprisMetadataChanged(parent()->property("mprisMetadata").toMap()); - return; - - case WatchConnector::musicSEND_NOW_PLAYING: - qCWarning(l) << "Operation" << operation << "not supported"; - return; - } - - if (method.isEmpty()) { - qCCritical(l) << "Requested unsupported operation" << operation; - return; - } - - qCDebug(l) << operation << "->" << method; - - QDBusError err = QDBusConnection::sessionBus().call( - QDBusMessage::createMethodCall(mpris, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", method)); - if (err.isValid()) { - qCCritical(l) << err.message(); - } -} diff --git a/daemon/watchcommands.h b/daemon/watchcommands.h deleted file mode 100644 index 1fa4859..0000000 --- a/daemon/watchcommands.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef WATCHCOMMANDS_H -#define WATCHCOMMANDS_H - -#include "watchconnector.h" -#include <QLoggingCategory> - -#include <QObject> - -class WatchCommands : public QObject -{ - Q_OBJECT - QLoggingCategory l; - - watch::WatchConnector *watch; - -public: - explicit WatchCommands(watch::WatchConnector *watch, QObject *parent = 0); - -signals: - void hangup(); - -public slots: - void processMessage(uint endpoint, QByteArray data); - -protected slots: - void onMprisMetadataChanged(QVariantMap metadata); - void musicControl(watch::WatchConnector::MusicControl operation); - -}; - -#endif // WATCHCOMMANDS_H diff --git a/daemon/watchconnector.cpp b/daemon/watchconnector.cpp index 7c2b272..ef032f7 100644 --- a/daemon/watchconnector.cpp +++ b/daemon/watchconnector.cpp @@ -1,17 +1,60 @@ -#include "watchconnector.h" -#include <QTimer> #include <QDateTime> #include <QMetaEnum> -using namespace watch; +#include "unpacker.h" +#include "watchconnector.h" -static int RECONNECT_TIMEOUT = 500; //ms +static const int RECONNECT_TIMEOUT = 500; //ms +static const bool PROTOCOL_DEBUG = false; WatchConnector::WatchConnector(QObject *parent) : QObject(parent), l(metaObject()->className()), socket(nullptr), is_connected(false) { reconnectTimer.setSingleShot(true); connect(&reconnectTimer, SIGNAL(timeout()), SLOT(reconnect())); + + setEndpointHandler(watchVERSION, [this](const QByteArray &data) { + Unpacker u(data); + + u.skip(1); + + quint32 version = u.read<quint32>(); + QString version_string = u.readFixedString(32); + QString commit = u.readFixedString(8); + bool is_recovery = u.read<quint8>(); + quint8 hw_platform = u.read<quint8>(); + quint8 metadata_version = u.read<quint8>(); + + quint32 safe_version = u.read<quint32>(); + QString safe_version_string = u.readFixedString(32); + QString safe_commit = u.readFixedString(8); + bool safe_is_recovery = u.read<quint8>(); + quint8 safe_hw_platform = u.read<quint8>(); + quint8 safe_metadata_version = u.read<quint8>(); + + quint32 bootLoaderTimestamp = u.read<quint32>(); + QString hardwareRevision = u.readFixedString(9); + QString serialNumber = u.readFixedString(12); + QByteArray address = u.readBytes(6); + + if (u.bad()) { + qCWarning(l) << "short read while reading firmware version"; + } + + qCDebug(l) << "got version information" + << version << version_string << commit + << is_recovery << hw_platform << metadata_version; + qCDebug(l) << "recovery version information" + << safe_version << safe_version_string << safe_commit + << safe_is_recovery << safe_hw_platform << safe_metadata_version; + qCDebug(l) << "hardware information" << bootLoaderTimestamp << hardwareRevision; + qCDebug(l) << "serial number" << serialNumber.left(3) << "..."; + qCDebug(l) << "bt address" << address.toHex(); + + this->_serialNumber = serialNumber; + + return true; + }); } WatchConnector::~WatchConnector() @@ -44,11 +87,11 @@ void WatchConnector::reconnect() void WatchConnector::disconnect() { - qCDebug(l) << __FUNCTION__; + qCDebug(l) << "disconnecting"; socket->close(); socket->deleteLater(); reconnectTimer.stop(); - qCDebug(l) << "Stopped reconnect timer"; + qCDebug(l) << "stopped reconnect timer"; } void WatchConnector::handleWatch(const QString &name, const QString &address) @@ -65,6 +108,11 @@ void WatchConnector::handleWatch(const QString &name, const QString &address) _last_address = address; if (emit_name) emit nameChanged(); + if (emit_name) { + // If we've changed names, don't reuse cached serial number! + _serialNumber.clear(); + } + qCDebug(l) << "Creating socket"; socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol); connect(socket, SIGNAL(readyRead()), SLOT(onReadSocket())); @@ -79,49 +127,109 @@ void WatchConnector::handleWatch(const QString &name, const QString &address) QString WatchConnector::decodeEndpoint(uint val) { - QMetaEnum Endpoints = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("Endpoints")); + QMetaEnum Endpoints = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("Endpoint")); const char *endpoint = Endpoints.valueToKey(val); return endpoint ? QString(endpoint) : QString("watchUNKNOWN_%1").arg(val); } -void WatchConnector::decodeMsg(QByteArray data) +void WatchConnector::setEndpointHandler(uint endpoint, const EndpointHandlerFunc &func) { - //Sometimes pebble sends a "00", we ignore it without future action - if (data.length() == 1 && data.at(0) == 0) { - return; - } - - if (data.length() < 4) { - qCCritical(l) << "Can not decode message data length invalid: " << data.toHex(); - return; + if (func) { + handlers.insert(endpoint, func); + } else { + handlers.remove(endpoint); } +} - unsigned int datalen = 0; - int index = 0; - datalen = (data.at(index) << 8) + data.at(index+1); - index += 2; +void WatchConnector::clearEndpointHandler(uint endpoint) +{ + handlers.remove(endpoint); +} - unsigned int endpoint = 0; - endpoint = (data.at(index) << 8) + data.at(index+1); - index += 2; +bool WatchConnector::dispatchMessage(uint endpoint, const QByteArray &data) +{ + auto tmp_it = tmpHandlers.find(endpoint); + if (tmp_it != tmpHandlers.end()) { + QList<EndpointHandlerFunc>& funcs = tmp_it.value(); + bool ok = false; + for (int i = 0; i < funcs.size(); i++) { + if (funcs[i](data)) { + // This handler accepted this message + ok = true; + // Since it is a temporary handler, remove it. + funcs.removeAt(i); + break; + } + } + if (funcs.empty()) { + // "Garbage collect" the tmpHandlers entry. + tmpHandlers.erase(tmp_it); + } + if (ok) { + return true; + } + } - qCDebug(l) << "Length:" << datalen << "Endpoint:" << decodeEndpoint(endpoint); - qCDebug(l) << "Data:" << data.mid(index).toHex(); + auto it = handlers.find(endpoint); + if (it != handlers.end()) { + if (it.value() && it.value()(data)) { + return true; + } + } - emit messageDecoded(endpoint, data.mid(index, datalen)); + qCDebug(l) << "message to endpoint" << decodeEndpoint(endpoint) << "was not dispatched"; + qCDebug(l) << data.toHex(); + return false; } void WatchConnector::onReadSocket() { - qCDebug(l) << "read"; + static const int header_length = 4; + + qCDebug(l) << "readyRead bytesAvailable =" << socket->bytesAvailable(); QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender()); - if (!socket) return; + Q_ASSERT(socket && socket == this->socket); + + // Keep attempting to read messages as long as at least a header is present + while (socket->bytesAvailable() >= header_length) { + // Take a look at the header, but do not remove it from the socket input buffer. + // We will only remove it once we're sure the entire packet is in the buffer. + uchar header[header_length]; + socket->peek(reinterpret_cast<char*>(header), header_length); + + quint16 message_length = qFromBigEndian<quint16>(&header[0]); + quint16 endpoint = qFromBigEndian<quint16>(&header[2]); + + // Sanity checks on the message_length + if (message_length == 0) { + qCWarning(l) << "received empty message"; + socket->read(header_length); // skip this header + continue; // check if there are additional headers. + } else if (message_length > 8 * 1024) { + // Protocol does not allow messages more than 8K long, seemingly. + qCWarning(l) << "received message size too long: " << message_length; + socket->readAll(); // drop entire input buffer + return; + } - while (socket->bytesAvailable()) { - QByteArray line = socket->readAll(); - emit messageReceived(socket->peerName(), QString::fromUtf8(line.constData(), line.length())); - decodeMsg(line); + // Now wait for the entire message + if (socket->bytesAvailable() < header_length + message_length) { + qCDebug(l) << "incomplete msg body in read buffer"; + return; // try again once more data comes in + } + + // We can now safely remove the header from the input buffer, + // as we know the entire message is in the input buffer. + socket->read(header_length); + + // Now read the rest of the message + QByteArray data = socket->read(message_length); + + qCDebug(l) << "received message of length" << message_length << "to endpoint" << decodeEndpoint(endpoint); + if (PROTOCOL_DEBUG) qCDebug(l) << data.toHex(); + + dispatchMessage(endpoint, data); } } @@ -132,11 +240,15 @@ void WatchConnector::onConnected() is_connected = true; reconnectTimer.stop(); reconnectTimer.setInterval(0); - if (not was_connected) { - if (not writeData.isEmpty()) { + if (!was_connected) { + if (!writeData.isEmpty()) { qCDebug(l) << "Found" << writeData.length() << "bytes in write buffer - resending"; sendData(writeData); } + if (_serialNumber.isEmpty()) { + // Ask for version information from the watch + sendMessage(watchVERSION, QByteArray(1, 0)); + } emit connectedChanged(); } } @@ -163,7 +275,7 @@ void WatchConnector::onDisconnected() reconnectTimer.setInterval(reconnectTimer.interval() + RECONNECT_TIMEOUT); } reconnectTimer.start(); - qCDebug(l) << "Will reconnect in" << reconnectTimer.interval() << "ms"; + qCDebug(l) << "will reconnect in" << reconnectTimer.interval() << "ms"; } void WatchConnector::onError(QBluetoothSocket::SocketError error) @@ -171,33 +283,32 @@ void WatchConnector::onError(QBluetoothSocket::SocketError error) if (error == QBluetoothSocket::UnknownSocketError) { qCDebug(l) << error << socket->errorString(); } else { - qCCritical(l) << "Error connecting Pebble:" << error << socket->errorString(); + qCCritical(l) << "error connecting Pebble:" << error << socket->errorString(); } } void WatchConnector::sendData(const QByteArray &data) { - writeData = data; + writeData.append(data); if (socket == nullptr) { - qCDebug(l) << "No socket - reconnecting"; + qCDebug(l) << "no socket - reconnecting"; reconnect(); - return; - } - if (is_connected) { - qCDebug(l) << "Writing" << data.length() << "bytes to socket"; + } else if (is_connected) { + qCDebug(l) << "writing" << data.length() << "bytes to socket"; + if (PROTOCOL_DEBUG) qCDebug(l) << data.toHex(); socket->write(data); } } void WatchConnector::onBytesWritten(qint64 bytes) { - writeData = writeData.mid(bytes); - qCDebug(l) << "Socket written" << bytes << "bytes," << writeData.length() << "left"; + writeData.remove(0, bytes); + qCDebug(l) << "socket written" << bytes << "bytes," << writeData.length() << "left"; } -void WatchConnector::sendMessage(uint endpoint, QByteArray data) +void WatchConnector::sendMessage(uint endpoint, const QByteArray &data, const EndpointHandlerFunc &callback) { - qCDebug(l) << "Sending message"; + qCDebug(l) << "sending message to endpoint" << decodeEndpoint(endpoint); QByteArray msg; // First send the length @@ -212,6 +323,10 @@ void WatchConnector::sendMessage(uint endpoint, QByteArray data) msg.append(data); sendData(msg); + + if (callback) { + tmpHandlers[endpoint].append(callback); + } } void WatchConnector::buildData(QByteArray &res, QStringList data) diff --git a/daemon/watchconnector.h b/daemon/watchconnector.h index fa8d18b..6c28e88 100644 --- a/daemon/watchconnector.h +++ b/daemon/watchconnector.h @@ -30,6 +30,7 @@ #ifndef WATCHCONNECTOR_H #define WATCHCONNECTOR_H +#include <functional> #include <QObject> #include <QPointer> #include <QStringList> @@ -39,21 +40,18 @@ #include <QBluetoothServiceInfo> #include <QLoggingCategory> -namespace watch -{ - class WatchConnector : public QObject { Q_OBJECT QLoggingCategory l; - Q_ENUMS(Endpoints) + Q_ENUMS(Endpoint) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString connected READ isConnected NOTIFY connectedChanged) public: - enum Endpoints { + enum Endpoint { watchTIME = 11, watchVERSION = 16, watchPHONE_VERSION = 17, @@ -73,6 +71,8 @@ public: watchAPP_MANAGER = 6000, watchDATA_LOGGING = 6778, watchSCREENSHOT = 8000, + watchFILE_MANAGER = 8181, + watchCORE_DUMP = 9000, watchPUTBYTES = 48879 }; enum { @@ -94,8 +94,7 @@ public: musicPREVIOUS = 5, musicVOLUME_UP = 6, musicVOLUME_DOWN = 7, - musicGET_NOW_PLAYING = 8, - musicSEND_NOW_PLAYING = 9 + musicGET_NOW_PLAYING = 8 }; enum SystemMessage { systemFIRMWARE_AVAILABLE = 0, @@ -107,6 +106,28 @@ public: systemBLUETOOTH_START_DISCOVERABLE = 6, systemBLUETOOTH_END_DISCOVERABLE = 7 }; + enum AppManager { + appmgrGET_APPBANK_STATUS = 1, + appmgrREMOVE_APP = 2, + appmgrREFRESH_APP = 3, + appmgrGET_APPBANK_UUIDS = 5 + }; + enum AppMessage { + appmsgPUSH = 1, + appmsgREQUEST = 2, + appmsgACK = 0xFF, + appmsgNACK = 0x7F + }; + enum DataLogMessage { + datalogOPEN = 1, + datalogDATA = 2, + datalogCLOSE = 3, + datalogTIMEOUT = 7 + }; + enum { + launcherSTARTED = 1, + launcherSTOPPED = 0 + }; enum { leadEMAIL = 0, leadSMS = 1, @@ -135,27 +156,59 @@ public: osLINUX = 4, osWINDOWS = 5 }; + enum UploadType { + uploadFIRMWARE = 1, + uploadRECOVERY = 2, + uploadSYS_RESOURCES = 3, + uploadRESOURCES = 4, + uploadBINARY = 5, + uploadFILE = 6, + uploadWORKER = 7 + }; + enum PutBytesCommand { + putbytesINIT = 1, + putbytesSEND = 2, + putbytesCOMMIT = 3, + putbytesABORT = 4, + putbytesCOMPLETE = 5 + }; + typedef QMap<int, QVariant> Dict; + enum DictItemType { + typeBYTES, + typeSTRING, + typeUINT, + typeINT + }; + + typedef std::function<bool(const QByteArray &)> EndpointHandlerFunc; explicit WatchConnector(QObject *parent = 0); virtual ~WatchConnector(); - bool isConnected() const { return is_connected; } - QString name() const { return socket != nullptr ? socket->peerName() : ""; } - QString timeStamp(); - QString decodeEndpoint(uint val); + inline bool isConnected() const { return is_connected; } + inline QString name() const { return socket != nullptr ? socket->peerName() : ""; } + inline QString serialNumber() const { return _serialNumber; } + + void setEndpointHandler(uint endpoint, const EndpointHandlerFunc &func); + void clearEndpointHandler(uint endpoint); + + static QString timeStamp(); + static QString decodeEndpoint(uint val); signals: - void messageReceived(QString peer, QString msg); - void messageDecoded(uint endpoint, QByteArray data); void nameChanged(); void connectedChanged(); public slots: - void sendData(const QByteArray &data); - void sendMessage(uint endpoint, QByteArray data); + void deviceConnect(const QString &name, const QString &address); + void disconnect(); + void reconnect(); + + void sendMessage(uint endpoint, const QByteArray &data, const EndpointHandlerFunc &callback = EndpointHandlerFunc()); void ping(uint val); void time(); + void sendNotification(uint lead, QString sender, QString data, QString subject); void sendSMSNotification(QString sender, QString data); void sendEmailNotification(QString sender, QString data, QString subject); @@ -172,8 +225,7 @@ public slots: void startPhoneCall(uint cookie=0); void endPhoneCall(uint cookie=0); - void deviceConnect(const QString &name, const QString &address); - void disconnect(); +private slots: void deviceDiscovered(const QBluetoothDeviceInfo&); void handleWatch(const QString &name, const QString &address); void onReadSocket(); @@ -181,18 +233,20 @@ public slots: void onConnected(); void onDisconnected(); void onError(QBluetoothSocket::SocketError error); - void reconnect(); private: - void decodeMsg(QByteArray data); + void sendData(const QByteArray &data); + bool dispatchMessage(uint endpoint, const QByteArray &data); QPointer<QBluetoothSocket> socket; + QHash<uint, QList<EndpointHandlerFunc>> tmpHandlers; + QHash<uint, EndpointHandlerFunc> handlers; bool is_connected; QByteArray writeData; QTimer reconnectTimer; QString _last_name; QString _last_address; + QString _serialNumber; }; -} #endif // WATCHCONNECTOR_H diff --git a/org.pebbled.Watch.xml b/org.pebbled.Watch.xml new file mode 100644 index 0000000..6336d04 --- /dev/null +++ b/org.pebbled.Watch.xml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node name="/org/pebbled/Watch"> + <interface name="org.pebbled.Watch"> + <property name="Name" type="s" access="read"/> + <property name="Address" type="s" access="read"/> + <property name="Connected" type="b" access="read"/> + <property name="AppUuid" type="s" access="read"/> + + <!-- Qt does not yet support property changed signals, so we fake them. --> + <signal name="NameChanged"/> + <signal name="AddressChanged"/> + <signal name="ConnectedChanged"/> + <signal name="AppUuidChanged"/> + + <method name="Disconnect"/> + <method name="Reconnect"/> + <method name="Ping"> + <arg name="val" type="u" direction="in"/> + </method> + <method name="SyncTime"/> + + <!-- The following methods resemble the official PebbleKit API --> + <method name="LaunchApp"> + <arg name="uuid" type="s" direction="in"/> + </method> + <method name="CloseApp"> + <arg name="uuid" type="s" direction="in"/> + </method> + <method name="SendAppMessage"> + <arg name="uuid" type="s" direction="in"/> + <arg name="message" type="a{sv}" direction="in"/> + <arg name="msg_ack" type="b" direction="out"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/> + </method> + <signal name="AppOpened"> + <arg name="uuid" type="s"/> + </signal> + <signal name="AppClosed"> + <arg name="uuid" type="s"/> + </signal> + + <!-- The following method are used for configuration of JSKit applications by the client app --> + <method name="StartAppConfiguration"> + <arg name="uuid" type="s" direction="in"/> + <arg name="config_url" type="s" direction="out"/> + </method> + <method name="SendAppConfigurationData"> + <arg name="uuid" type="s" direction="in"/> + <arg name="data" type="s" direction="in"/> + </method> + + <!-- Bank management methods --> + <property name="AppSlots" type="as" access="read"/> + <signal name="AppSlotsChanged"/> + <method name="UnloadApp"> + <arg name="slot" type="i" direction="in"/> + </method> + <method name="UploadApp"> + <arg name="uuid" type="s" direction="in"/> + <arg name="slot" type="i" direction="in"/> + </method> + + <!-- Installed apps (not necessarily uploaded) --> + <property name="AllApps" type="aa{sv}" access="read"> + <annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantList"/> + </property> + <signal name="AllAppsChanged"/> + </interface> +</node> diff --git a/rpm/pebble.spec b/rpm/pebble.spec index ae28945..bd86a5c 100644 --- a/rpm/pebble.spec +++ b/rpm/pebble.spec @@ -13,7 +13,7 @@ Name: pebble %{!?qtc_make:%define qtc_make make} %{?qtc_builddir:%define _builddir %qtc_builddir} Summary: Support for Pebble watch in SailfishOS -Version: 0.13 +Version: 0.13.js1 Release: 1 Group: Qt/Qt License: GPL3 @@ -30,6 +30,7 @@ BuildRequires: pkgconfig(Qt5Qml) BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(mlite5) BuildRequires: pkgconfig(sailfishapp) >= 0.0.10 +BuildRequires: pkgconfig(icu-i18n) BuildRequires: desktop-file-utils %description @@ -78,6 +79,8 @@ systemctl --user daemon-reload %defattr(-,root,root,-) %{_bindir} %{_datadir}/%{name}/qml +%{_datadir}/%{name}/js +%{_datadir}/%{name}/translations %{_datadir}/applications/%{name}.desktop %{_datadir}/icons/hicolor/86x86/apps/%{name}.png %{_libdir}/systemd/user/%{name}d.service diff --git a/rpm/pebble.yaml b/rpm/pebble.yaml index f4b8db1..4681e9c 100644 --- a/rpm/pebble.yaml +++ b/rpm/pebble.yaml @@ -1,6 +1,6 @@ Name: pebble Summary: Support for Pebble watch in SailfishOS -Version: 0.13 +Version: 0.13.js1 Release: 1 Group: Qt/Qt URL: http://getpebble.com/ @@ -22,16 +22,16 @@ PkgConfigBR: - Qt5Core - mlite5 - sailfishapp >= 0.0.10 -PkgBR: -- libicu-devel +- icu-i18n Requires: - sailfishsilica-qt5 >= 0.10.9 - systemd-user-session-targets Files: - '%{_bindir}' - '%{_datadir}/%{name}/qml' +- '%{_datadir}/%{name}/js' +- '%{_datadir}/%{name}/translations' - '%{_datadir}/applications/%{name}.desktop' - '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png' - '%{_libdir}/systemd/user/%{name}d.service' - '%{_libdir}/systemd/user/user-session.target.wants/%{name}d.service' -PkgBR: [] |
