summaryrefslogtreecommitdiff
path: root/daemon
diff options
context:
space:
mode:
Diffstat (limited to 'daemon')
-rw-r--r--daemon/appinfo.cpp188
-rw-r--r--daemon/appinfo.h86
-rw-r--r--daemon/appmanager.cpp267
-rw-r--r--daemon/appmanager.h42
-rw-r--r--daemon/appmsgmanager.cpp389
-rw-r--r--daemon/appmsgmanager.h77
-rw-r--r--daemon/bankmanager.cpp373
-rw-r--r--daemon/bankmanager.h63
-rw-r--r--daemon/daemon.cpp10
-rw-r--r--daemon/daemon.pro54
-rw-r--r--daemon/datalogmanager.cpp42
-rw-r--r--daemon/datalogmanager.h25
-rw-r--r--daemon/dbusadaptor.cpp84
-rw-r--r--daemon/dbusadaptor.h78
-rw-r--r--daemon/dbusconnector.cpp27
-rw-r--r--daemon/dbusconnector.h14
-rw-r--r--daemon/js/typedarray.js1030
-rw-r--r--daemon/jskitmanager.cpp205
-rw-r--r--daemon/jskitmanager.h60
-rw-r--r--daemon/jskitobjects.cpp773
-rw-r--r--daemon/jskitobjects.h221
-rw-r--r--daemon/manager.cpp320
-rw-r--r--daemon/manager.h122
-rw-r--r--daemon/musicmanager.cpp221
-rw-r--r--daemon/musicmanager.h36
-rw-r--r--daemon/org.pebbled.xml20
-rw-r--r--daemon/packer.cpp132
-rw-r--r--daemon/packer.h69
-rw-r--r--daemon/settings.h29
-rw-r--r--daemon/stm32crc.cpp156
-rw-r--r--daemon/stm32crc.h24
-rw-r--r--daemon/unpacker.cpp90
-rw-r--r--daemon/unpacker.h92
-rw-r--r--daemon/uploadmanager.cpp300
-rw-r--r--daemon/uploadmanager.h74
-rw-r--r--daemon/voicecallmanager.cpp7
-rw-r--r--daemon/voicecallmanager.h1
-rw-r--r--daemon/watchcommands.cpp122
-rw-r--r--daemon/watchcommands.h31
-rw-r--r--daemon/watchconnector.cpp207
-rw-r--r--daemon/watchconnector.h94
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, &notifications, &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