diff options
Diffstat (limited to 'daemon')
41 files changed, 5649 insertions, 606 deletions
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 |
