summaryrefslogtreecommitdiff
path: root/rockworkd
diff options
context:
space:
mode:
Diffstat (limited to 'rockworkd')
-rw-r--r--rockworkd/core.cpp51
-rw-r--r--rockworkd/core.h33
-rw-r--r--rockworkd/dbusinterface.cpp308
-rw-r--r--rockworkd/dbusinterface.h102
-rw-r--r--rockworkd/jsfiles.qrc2
-rw-r--r--rockworkd/libpebble/appdownloader.cpp113
-rw-r--r--rockworkd/libpebble/appdownloader.h32
-rw-r--r--rockworkd/libpebble/appinfo.cpp163
-rw-r--r--rockworkd/libpebble/appinfo.h57
-rw-r--r--rockworkd/libpebble/appmanager.cpp255
-rw-r--r--rockworkd/libpebble/appmanager.h80
-rw-r--r--rockworkd/libpebble/appmetadata.cpp73
-rw-r--r--rockworkd/libpebble/appmetadata.h39
-rw-r--r--rockworkd/libpebble/appmsgmanager.cpp461
-rw-r--r--rockworkd/libpebble/appmsgmanager.h94
-rw-r--r--rockworkd/libpebble/blobdb.cpp584
-rw-r--r--rockworkd/libpebble/blobdb.h108
-rw-r--r--rockworkd/libpebble/bluez/bluez_adapter1.cpp26
-rw-r--r--rockworkd/libpebble/bluez/bluez_adapter1.h66
-rw-r--r--rockworkd/libpebble/bluez/bluez_agentmanager1.cpp26
-rw-r--r--rockworkd/libpebble/bluez/bluez_agentmanager1.h68
-rw-r--r--rockworkd/libpebble/bluez/bluez_device1.cpp26
-rw-r--r--rockworkd/libpebble/bluez/bluez_device1.h85
-rw-r--r--rockworkd/libpebble/bluez/bluez_helper.h30
-rw-r--r--rockworkd/libpebble/bluez/bluezclient.cpp84
-rw-r--r--rockworkd/libpebble/bluez/bluezclient.h51
-rw-r--r--rockworkd/libpebble/bluez/dbus-shared.h36
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp26
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_objectmanager.h58
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_properties.cpp26
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_properties.h71
-rw-r--r--rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml16
-rw-r--r--rockworkd/libpebble/bundle.cpp151
-rw-r--r--rockworkd/libpebble/bundle.h33
-rw-r--r--rockworkd/libpebble/calendarevent.cpp184
-rw-r--r--rockworkd/libpebble/calendarevent.h69
-rw-r--r--rockworkd/libpebble/dataloggingendpoint.cpp44
-rw-r--r--rockworkd/libpebble/dataloggingendpoint.h39
-rw-r--r--rockworkd/libpebble/enums.h95
-rw-r--r--rockworkd/libpebble/firmwaredownloader.cpp246
-rw-r--r--rockworkd/libpebble/firmwaredownloader.h50
-rw-r--r--rockworkd/libpebble/healthparams.cpp93
-rw-r--r--rockworkd/libpebble/healthparams.h52
-rw-r--r--rockworkd/libpebble/jskit/cacheLocalStorage.js11
-rw-r--r--rockworkd/libpebble/jskit/jsfiles.qrc7
-rw-r--r--rockworkd/libpebble/jskit/jskitconsole.cpp29
-rw-r--r--rockworkd/libpebble/jskit/jskitconsole.h20
-rw-r--r--rockworkd/libpebble/jskit/jskitgeolocation.cpp302
-rw-r--r--rockworkd/libpebble/jskit/jskitgeolocation.h66
-rw-r--r--rockworkd/libpebble/jskit/jskitlocalstorage.cpp117
-rw-r--r--rockworkd/libpebble/jskit/jskitlocalstorage.h40
-rw-r--r--rockworkd/libpebble/jskit/jskitmanager.cpp240
-rw-r--r--rockworkd/libpebble/jskit/jskitmanager.h72
-rw-r--r--rockworkd/libpebble/jskit/jskitpebble.cpp355
-rw-r--r--rockworkd/libpebble/jskit/jskitpebble.h47
-rw-r--r--rockworkd/libpebble/jskit/jskitperformance.cpp13
-rw-r--r--rockworkd/libpebble/jskit/jskitperformance.h20
-rw-r--r--rockworkd/libpebble/jskit/jskitsetup.js196
-rw-r--r--rockworkd/libpebble/jskit/jskittimer.cpp77
-rw-r--r--rockworkd/libpebble/jskit/jskittimer.h31
-rw-r--r--rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp318
-rw-r--r--rockworkd/libpebble/jskit/jskitxmlhttprequest.h96
-rw-r--r--rockworkd/libpebble/jskit/typedarray.js1037
-rw-r--r--rockworkd/libpebble/musicendpoint.cpp63
-rw-r--r--rockworkd/libpebble/musicendpoint.h37
-rw-r--r--rockworkd/libpebble/musicmetadata.cpp14
-rw-r--r--rockworkd/libpebble/musicmetadata.h17
-rw-r--r--rockworkd/libpebble/notification.cpp79
-rw-r--r--rockworkd/libpebble/notification.h59
-rw-r--r--rockworkd/libpebble/notificationendpoint.cpp46
-rw-r--r--rockworkd/libpebble/notificationendpoint.h64
-rw-r--r--rockworkd/libpebble/pebble.cpp693
-rw-r--r--rockworkd/libpebble/pebble.h225
-rw-r--r--rockworkd/libpebble/phonecallendpoint.cpp71
-rw-r--r--rockworkd/libpebble/phonecallendpoint.h47
-rw-r--r--rockworkd/libpebble/platforminterface.h46
-rw-r--r--rockworkd/libpebble/screenshotendpoint.cpp131
-rw-r--r--rockworkd/libpebble/screenshotendpoint.h52
-rw-r--r--rockworkd/libpebble/timelineitem.cpp144
-rw-r--r--rockworkd/libpebble/timelineitem.h194
-rw-r--r--rockworkd/libpebble/uploadmanager.cpp331
-rw-r--r--rockworkd/libpebble/uploadmanager.h85
-rw-r--r--rockworkd/libpebble/watchconnection.cpp242
-rw-r--r--rockworkd/libpebble/watchconnection.h154
-rw-r--r--rockworkd/libpebble/watchdatareader.cpp6
-rw-r--r--rockworkd/libpebble/watchdatareader.h146
-rw-r--r--rockworkd/libpebble/watchdatawriter.cpp144
-rw-r--r--rockworkd/libpebble/watchdatawriter.h69
-rw-r--r--rockworkd/libpebble/watchlogendpoint.cpp128
-rw-r--r--rockworkd/libpebble/watchlogendpoint.h76
-rw-r--r--rockworkd/libpebble/ziphelper.cpp91
-rw-r--r--rockworkd/libpebble/ziphelper.h15
-rw-r--r--rockworkd/main.cpp22
-rw-r--r--rockworkd/pebblemanager.cpp95
-rw-r--r--rockworkd/pebblemanager.h35
-rw-r--r--rockworkd/platformintegration/testing/testingplatform.cpp63
-rw-r--r--rockworkd/platformintegration/testing/testingplatform.h31
-rw-r--r--rockworkd/platformintegration/testing/testui.qrc6
-rw-r--r--rockworkd/platformintegration/testing/testui/Main.qml87
-rw-r--r--rockworkd/platformintegration/testing/testui/PebbleController.qml44
-rw-r--r--rockworkd/platformintegration/ubuntu/callchannelobserver.cpp165
-rw-r--r--rockworkd/platformintegration/ubuntu/callchannelobserver.h74
-rw-r--r--rockworkd/platformintegration/ubuntu/organizeradapter.cpp74
-rw-r--r--rockworkd/platformintegration/ubuntu/organizeradapter.h33
-rw-r--r--rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp100
-rw-r--r--rockworkd/platformintegration/ubuntu/syncmonitorclient.h51
-rw-r--r--rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp232
-rw-r--r--rockworkd/platformintegration/ubuntu/ubuntuplatform.h62
-rw-r--r--rockworkd/rockworkd.pro146
109 files changed, 12189 insertions, 0 deletions
diff --git a/rockworkd/core.cpp b/rockworkd/core.cpp
new file mode 100644
index 0000000..38a25c5
--- /dev/null
+++ b/rockworkd/core.cpp
@@ -0,0 +1,51 @@
+#include "core.h"
+
+#include "pebblemanager.h"
+#include "dbusinterface.h"
+
+#include "platformintegration/ubuntu/ubuntuplatform.h"
+#ifdef ENABLE_TESTING
+#include "platformintegration/testing/testingplatform.h"
+#endif
+
+#include <QDebug>
+
+Core* Core::s_instance = nullptr;
+
+Core *Core::instance()
+{
+ if (!s_instance) {
+ s_instance = new Core();
+ }
+ return s_instance;
+}
+
+PebbleManager *Core::pebbleManager()
+{
+ return m_pebbleManager;
+}
+
+PlatformInterface *Core::platform()
+{
+ return m_platform;
+}
+
+Core::Core(QObject *parent):
+ QObject(parent)
+{
+}
+
+void Core::init()
+{
+ // Platform integration
+#ifdef ENABLE_TESTING
+ m_platform = new TestingPlatform(this);
+#else
+ m_platform = new UbuntuPlatform(this);
+#endif
+
+ m_pebbleManager = new PebbleManager(this);
+
+ m_dbusInterface = new DBusInterface(this);
+}
+
diff --git a/rockworkd/core.h b/rockworkd/core.h
new file mode 100644
index 0000000..4791aac
--- /dev/null
+++ b/rockworkd/core.h
@@ -0,0 +1,33 @@
+#ifndef CORE_H
+#define CORE_H
+
+#include <QObject>
+#include <QTimer>
+
+class PebbleManager;
+class DBusInterface;
+class PlatformInterface;
+
+class Core : public QObject
+{
+ Q_OBJECT
+public:
+ static Core *instance();
+
+ PebbleManager* pebbleManager();
+ PlatformInterface* platform();
+
+ void init();
+private:
+ explicit Core(QObject *parent = 0);
+ static Core *s_instance;
+
+private slots:
+
+private:
+ PebbleManager *m_pebbleManager;
+ DBusInterface *m_dbusInterface;
+ PlatformInterface *m_platform;
+};
+
+#endif // CORE_H
diff --git a/rockworkd/dbusinterface.cpp b/rockworkd/dbusinterface.cpp
new file mode 100644
index 0000000..766de62
--- /dev/null
+++ b/rockworkd/dbusinterface.cpp
@@ -0,0 +1,308 @@
+#include "dbusinterface.h"
+#include "core.h"
+#include "pebblemanager.h"
+
+DBusPebble::DBusPebble(Pebble *pebble, QObject *parent):
+ QObject(parent),
+ m_pebble(pebble)
+{
+ connect(pebble, &Pebble::pebbleConnected, this, &DBusPebble::Connected);
+ connect(pebble, &Pebble::pebbleDisconnected, this, &DBusPebble::Disconnected);
+ connect(pebble, &Pebble::installedAppsChanged, this, &DBusPebble::InstalledAppsChanged);
+ connect(pebble, &Pebble::openURL, this, &DBusPebble::OpenURL);
+ connect(pebble, &Pebble::notificationFilterChanged, this, &DBusPebble::NotificationFilterChanged);
+ connect(pebble, &Pebble::screenshotAdded, this, &DBusPebble::ScreenshotAdded);
+ connect(pebble, &Pebble::screenshotRemoved, this, &DBusPebble::ScreenshotRemoved);
+ connect(pebble, &Pebble::updateAvailableChanged, this, &DBusPebble::FirmwareUpgradeAvailableChanged);
+ connect(pebble, &Pebble::upgradingFirmwareChanged, this, &DBusPebble::UpgradingFirmwareChanged);
+ connect(pebble, &Pebble::logsDumped, this, &DBusPebble::LogsDumped);
+ connect(pebble, &Pebble::healtParamsChanged, this, &DBusPebble::HealthParamsChanged);
+ connect(pebble, &Pebble::imperialUnitsChanged, this, &DBusPebble::ImperialUnitsChanged);
+ connect(pebble, &Pebble::calendarSyncEnabledChanged, this, &DBusPebble::CalendarSyncEnabledChanged);
+}
+
+QString DBusPebble::Address() const
+{
+ return m_pebble->address().toString();
+}
+
+QString DBusPebble::Name() const
+{
+ return m_pebble->name();
+}
+
+bool DBusPebble::IsConnected() const
+{
+ return m_pebble->connected();
+}
+
+bool DBusPebble::Recovery() const
+{
+ return m_pebble->recovery();
+}
+
+bool DBusPebble::FirmwareUpgradeAvailable() const
+{
+ return m_pebble->firmwareUpdateAvailable();
+}
+
+QString DBusPebble::FirmwareReleaseNotes() const
+{
+ return m_pebble->firmwareReleaseNotes();
+}
+
+QString DBusPebble::CandidateFirmwareVersion() const
+{
+ return m_pebble->candidateFirmwareVersion();
+}
+
+QVariantMap DBusPebble::NotificationsFilter() const
+{
+ QVariantMap ret;
+ QHash<QString, bool> filter = m_pebble->notificationsFilter();
+ foreach (const QString &sourceId, filter.keys()) {
+ ret.insert(sourceId, filter.value(sourceId));
+ }
+ return ret;
+}
+
+void DBusPebble::SetNotificationFilter(const QString &sourceId, bool enabled)
+{
+ m_pebble->setNotificationFilter(sourceId, enabled);
+}
+
+void DBusPebble::InstallApp(const QString &id)
+{
+ qDebug() << "installapp called" << id;
+ m_pebble->installApp(id);
+}
+
+void DBusPebble::SideloadApp(const QString &packageFile)
+{
+ m_pebble->sideloadApp(packageFile);
+}
+
+QStringList DBusPebble::InstalledAppIds() const
+{
+ QStringList ret;
+ foreach (const QUuid &id, m_pebble->installedAppIds()) {
+ ret << id.toString();
+ }
+ return ret;
+}
+
+QVariantList DBusPebble::InstalledApps() const
+{
+ QVariantList list;
+ foreach (const QUuid &appId, m_pebble->installedAppIds()) {
+ QVariantMap app;
+ AppInfo info = m_pebble->appInfo(appId);
+ app.insert("storeId", info.storeId());
+ app.insert("name", info.shortName());
+ app.insert("vendor", info.companyName());
+ app.insert("watchface", info.isWatchface());
+ app.insert("version", info.versionLabel());
+ app.insert("uuid", info.uuid().toString());
+ app.insert("hasSettings", info.hasSettings());
+ app.insert("icon", info.path() + "/list_image.png");
+ app.insert("systemApp", info.isSystemApp());
+
+ list.append(app);
+ }
+ return list;
+}
+
+void DBusPebble::RemoveApp(const QString &id)
+{
+ m_pebble->removeApp(id);
+}
+
+void DBusPebble::ConfigurationURL(const QString &uuid)
+{
+ m_pebble->requestConfigurationURL(QUuid(uuid));
+}
+
+void DBusPebble::ConfigurationClosed(const QString &uuid, const QString &result)
+{
+ m_pebble->configurationClosed(QUuid(uuid), result);
+}
+
+void DBusPebble::SetAppOrder(const QStringList &newList)
+{
+ QList<QUuid> uuidList;
+ foreach (const QString &id, newList) {
+ uuidList << QUuid(id);
+ }
+ m_pebble->setAppOrder(uuidList);
+}
+
+void DBusPebble::LaunchApp(const QString &uuid)
+{
+ m_pebble->launchApp(QUuid(uuid));
+}
+
+void DBusPebble::RequestScreenshot()
+{
+ m_pebble->requestScreenshot();
+}
+
+QStringList DBusPebble::Screenshots() const
+{
+ return m_pebble->screenshots();
+}
+
+void DBusPebble::RemoveScreenshot(const QString &filename)
+{
+ qDebug() << "Should remove screenshot" << filename;
+ m_pebble->removeScreenshot(filename);
+}
+
+void DBusPebble::PerformFirmwareUpgrade()
+{
+ m_pebble->upgradeFirmware();
+}
+
+bool DBusPebble::UpgradingFirmware() const
+{
+ return m_pebble->upgradingFirmware();
+}
+
+QString DBusPebble::SerialNumber() const
+{
+ return m_pebble->serialNumber();
+}
+
+QString DBusPebble::HardwarePlatform() const
+{
+ switch (m_pebble->hardwarePlatform()) {
+ case HardwarePlatformAplite:
+ return "aplite";
+ case HardwarePlatformBasalt:
+ return "basalt";
+ case HardwarePlatformChalk:
+ return "chalk";
+ default:
+ ;
+ }
+ return "unknown";
+}
+
+QString DBusPebble::SoftwareVersion() const
+{
+ return m_pebble->softwareVersion();
+}
+
+int DBusPebble::Model() const
+{
+ return m_pebble->model();
+}
+
+void DBusPebble::DumpLogs(const QString &fileName) const
+{
+ qDebug() << "dumplogs" << fileName;
+ m_pebble->dumpLogs(fileName);
+}
+
+QVariantMap DBusPebble::HealthParams() const
+{
+ QVariantMap map;
+ map.insert("enabled", m_pebble->healthParams().enabled());
+ map.insert("age", m_pebble->healthParams().age());
+ map.insert("gender", m_pebble->healthParams().gender() == HealthParams::GenderFemale ? "female" : "male");
+ map.insert("height", m_pebble->healthParams().height());
+ map.insert("moreActive", m_pebble->healthParams().moreActive());
+ map.insert("sleepMore", m_pebble->healthParams().sleepMore());
+ map.insert("weight", m_pebble->healthParams().weight());
+ return map;
+}
+
+void DBusPebble::SetHealthParams(const QVariantMap &healthParams)
+{
+ ::HealthParams params;
+ params.setEnabled(healthParams.value("enabled").toBool());
+ params.setAge(healthParams.value("age").toInt());
+ params.setGender(healthParams.value("gender").toString() == "female" ? HealthParams::GenderFemale : HealthParams::GenderMale);
+ params.setHeight(healthParams.value("height").toInt());
+ params.setWeight(healthParams.value("weight").toInt());
+ params.setMoreActive(healthParams.value("moreActive").toBool());
+ params.setSleepMore(healthParams.value("sleepMore").toBool());
+ m_pebble->setHealthParams(params);
+}
+
+bool DBusPebble::ImperialUnits() const
+{
+ return m_pebble->imperialUnits();
+}
+
+void DBusPebble::SetImperialUnits(bool imperialUnits)
+{
+ qDebug() << "setting imperial units" << imperialUnits;
+ m_pebble->setImperialUnits(imperialUnits);
+}
+
+bool DBusPebble::CalendarSyncEnabled() const
+{
+ return m_pebble->calendarSyncEnabled();
+}
+
+void DBusPebble::SetCalendarSyncEnabled(bool enabled)
+{
+ m_pebble->setCalendarSyncEnabled(enabled);
+}
+
+
+DBusInterface::DBusInterface(QObject *parent) :
+ QObject(parent)
+{
+ QDBusConnection::sessionBus().registerService("org.rockwork");
+ QDBusConnection::sessionBus().registerObject("/org/rockwork/Manager", this, QDBusConnection::ExportScriptableSlots|QDBusConnection::ExportScriptableSignals);
+
+ qDebug() << "pebble manager has:" << Core::instance()->pebbleManager()->pebbles().count() << Core::instance()->pebbleManager();
+ foreach (Pebble *pebble, Core::instance()->pebbleManager()->pebbles()) {
+ pebbleAdded(pebble);
+ }
+
+ qDebug() << "connecting dbus iface";
+ connect(Core::instance()->pebbleManager(), &PebbleManager::pebbleAdded, this, &DBusInterface::pebbleAdded);
+ connect(Core::instance()->pebbleManager(), &PebbleManager::pebbleRemoved, this, &DBusInterface::pebbleRemoved);
+}
+
+QString DBusInterface::Version()
+{
+ return QStringLiteral(VERSION);
+}
+
+QList<QDBusObjectPath> DBusInterface::ListWatches()
+{
+ QList<QDBusObjectPath> ret;
+ foreach (const QString &address, m_dbusPebbles.keys()) {
+ ret.append(QDBusObjectPath("/org/rockwork/" + address));
+ }
+ return ret;
+}
+
+void DBusInterface::pebbleAdded(Pebble *pebble)
+{
+ qDebug() << "pebble added";
+ QString address = pebble->address().toString().replace(":", "_");
+ if (m_dbusPebbles.contains(address)) {
+ return;
+ }
+
+ qDebug() << "registering dbus iface";
+ DBusPebble *dbusPebble = new DBusPebble(pebble, this);
+ m_dbusPebbles.insert(address, dbusPebble);
+ QDBusConnection::sessionBus().registerObject("/org/rockwork/" + address, dbusPebble, QDBusConnection::ExportAllContents);
+
+ emit PebblesChanged();
+}
+
+void DBusInterface::pebbleRemoved(Pebble *pebble)
+{
+ QString address = pebble->address().toString().replace(":", "_");
+
+ QDBusConnection::sessionBus().unregisterObject("/org/rockwork/" + address);
+ m_dbusPebbles.remove(address);
+
+ emit PebblesChanged();
+}
diff --git a/rockworkd/dbusinterface.h b/rockworkd/dbusinterface.h
new file mode 100644
index 0000000..0c340f2
--- /dev/null
+++ b/rockworkd/dbusinterface.h
@@ -0,0 +1,102 @@
+#ifndef DBUSINTERFACE_H
+#define DBUSINTERFACE_H
+
+#include <QObject>
+#include <QDBusAbstractAdaptor>
+#include <QDBusObjectPath>
+
+class Pebble;
+
+class DBusPebble: public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.rockwork.Pebble")
+public:
+ DBusPebble(Pebble *pebble, QObject *parent);
+
+signals:
+ void Connected();
+ void Disconnected();
+ void NotificationFilterChanged(const QString &sourceId, bool enabled);
+ void InstalledAppsChanged();
+ void OpenURL(const QString &uuid, const QString &url);
+ void ScreenshotAdded(const QString &filename);
+ void ScreenshotRemoved(const QString &filename);
+ void FirmwareUpgradeAvailableChanged();
+ void UpgradingFirmwareChanged();
+ void LogsDumped(bool success);
+
+ void HealthParamsChanged();
+ void ImperialUnitsChanged();
+ void CalendarSyncEnabledChanged();
+
+public slots:
+ QString Address() const;
+ QString Name() const;
+ QString SerialNumber() const;
+ QString HardwarePlatform() const;
+ QString SoftwareVersion() const;
+ int Model() const;
+ bool IsConnected() const;
+ bool Recovery() const;
+ bool FirmwareUpgradeAvailable() const;
+ QString CandidateFirmwareVersion() const;
+ QString FirmwareReleaseNotes() const;
+ void PerformFirmwareUpgrade();
+ bool UpgradingFirmware() const;
+
+ QVariantMap NotificationsFilter() const;
+ void SetNotificationFilter(const QString &sourceId, bool enabled);
+
+ void InstallApp(const QString &id);
+ void SideloadApp(const QString &packageFile);
+ QStringList InstalledAppIds() const;
+ QVariantList InstalledApps() const;
+ void RemoveApp(const QString &id);
+ void ConfigurationURL(const QString &uuid);
+ void ConfigurationClosed(const QString &uuid, const QString &result);
+ void SetAppOrder(const QStringList &newList);
+ void LaunchApp(const QString &uuid);
+ void RequestScreenshot();
+ QStringList Screenshots() const;
+ void RemoveScreenshot(const QString &filename);
+ void DumpLogs(const QString &fileName) const;
+
+ QVariantMap HealthParams() const;
+ void SetHealthParams(const QVariantMap &healthParams);
+
+ bool ImperialUnits() const;
+ void SetImperialUnits(bool imperialUnits);
+
+ bool CalendarSyncEnabled() const;
+ void SetCalendarSyncEnabled(bool enabled);
+
+private:
+ Pebble *m_pebble;
+};
+
+class DBusInterface : public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.rockwork.Manager")
+
+public:
+ explicit DBusInterface(QObject *parent = 0);
+
+public slots:
+ Q_SCRIPTABLE QString Version();
+ Q_SCRIPTABLE QList<QDBusObjectPath> ListWatches();
+
+signals:
+ Q_SCRIPTABLE void PebblesChanged();
+ void NameChanged();
+
+private slots:
+ void pebbleAdded(Pebble *pebble);
+ void pebbleRemoved(Pebble *pebble);
+
+private:
+ QHash<QString, DBusPebble*> m_dbusPebbles;
+};
+
+#endif // DBUSINTERFACE_H
diff --git a/rockworkd/jsfiles.qrc b/rockworkd/jsfiles.qrc
new file mode 100644
index 0000000..807350d
--- /dev/null
+++ b/rockworkd/jsfiles.qrc
@@ -0,0 +1,2 @@
+<RCC/>
+
diff --git a/rockworkd/libpebble/appdownloader.cpp b/rockworkd/libpebble/appdownloader.cpp
new file mode 100644
index 0000000..acecf0f
--- /dev/null
+++ b/rockworkd/libpebble/appdownloader.cpp
@@ -0,0 +1,113 @@
+#include "appdownloader.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "ziphelper.h"
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QDir>
+#include <QFile>
+#include <QJsonDocument>
+
+AppDownloader::AppDownloader(const QString &storagePath, QObject *parent) :
+ QObject(parent),
+ m_storagePath(storagePath + "/apps/")
+{
+ m_nam = new QNetworkAccessManager(this);
+}
+
+void AppDownloader::downloadApp(const QString &id)
+{
+ QNetworkRequest request(QUrl("https://api2.getpebble.com/v2/apps/id/" + id));
+ QNetworkReply *reply = m_nam->get(request);
+ reply->setProperty("storeId", id);
+ connect(reply, &QNetworkReply::finished, this, &AppDownloader::appJsonFetched);
+}
+
+void AppDownloader::appJsonFetched()
+{
+ QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
+ reply->deleteLater();
+
+ QString storeId = reply->property("storeId").toString();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qWarning() << "Error fetching App Json" << reply->errorString();
+ return;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing App Json" << error.errorString();
+ return;
+ }
+
+ QVariantMap map = jsonDoc.toVariant().toMap();
+ if (!map.contains("data") || map.value("data").toList().length() == 0) {
+ qWarning() << "Unexpected json content:" << jsonDoc.toJson();
+ return;
+ }
+ QVariantMap appMap = map.value("data").toList().first().toMap();
+ QString pbwFileUrl = appMap.value("latest_release").toMap().value("pbw_file").toString();
+ if (pbwFileUrl.isEmpty()) {
+ qWarning() << "pbw file url empty." << jsonDoc.toJson();
+ return;
+ }
+
+ QDir dir;
+ dir.mkpath(m_storagePath + storeId);
+
+ QString iconFile = appMap.value("list_image").toMap().value("144x144").toString();
+ QNetworkRequest request(iconFile);
+ QNetworkReply *imageReply = m_nam->get(request);
+ qDebug() << "fetching image" << iconFile;
+ connect(imageReply, &QNetworkReply::finished, [this, imageReply, storeId]() {
+ imageReply->deleteLater();
+ QString targetFile = m_storagePath + storeId + "/list_image.png";
+ qDebug() << "saving image to" << targetFile;
+ QFile f(targetFile);
+ if (f.open(QFile::WriteOnly)) {
+ f.write(imageReply->readAll());
+ f.close();
+ }
+ });
+
+ fetchPackage(pbwFileUrl, storeId);
+}
+
+void AppDownloader::fetchPackage(const QString &url, const QString &storeId)
+{
+ QNetworkRequest request(url);
+ QNetworkReply *reply = m_nam->get(request);
+ reply->setProperty("storeId", storeId);
+ connect(reply, &QNetworkReply::finished, this, &AppDownloader::packageFetched);
+}
+
+void AppDownloader::packageFetched()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ reply->deleteLater();
+
+ QString storeId = reply->property("storeId").toString();
+
+ QFile f(m_storagePath + storeId + "/" + reply->request().url().fileName() + ".zip");
+ if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Error opening file for writing";
+ return;
+ }
+ f.write(reply->readAll());
+ f.flush();
+ f.close();
+
+ QString zipName = m_storagePath + storeId + "/" + reply->request().url().fileName() + ".zip";
+
+ if (!ZipHelper::unpackArchive(zipName, m_storagePath + storeId)) {
+ qWarning() << "Error unpacking App zip file";
+ return;
+ }
+
+ emit downloadFinished(storeId);
+}
diff --git a/rockworkd/libpebble/appdownloader.h b/rockworkd/libpebble/appdownloader.h
new file mode 100644
index 0000000..6c81c4a
--- /dev/null
+++ b/rockworkd/libpebble/appdownloader.h
@@ -0,0 +1,32 @@
+#ifndef APPDOWNLOADER_H
+#define APPDOWNLOADER_H
+
+#include <QObject>
+#include <QMap>
+
+class QNetworkAccessManager;
+
+class AppDownloader : public QObject
+{
+ Q_OBJECT
+public:
+ explicit AppDownloader(const QString &storagePath, QObject *parent = 0);
+
+public slots:
+ void downloadApp(const QString &id);
+
+signals:
+ void downloadFinished(const QString &id);
+
+private slots:
+ void appJsonFetched();
+ void packageFetched();
+
+private:
+ void fetchPackage(const QString &url, const QString &storeId);
+
+ QNetworkAccessManager *m_nam;
+ QString m_storagePath;
+};
+
+#endif // APPDOWNLOADER_H
diff --git a/rockworkd/libpebble/appinfo.cpp b/rockworkd/libpebble/appinfo.cpp
new file mode 100644
index 0000000..4aeeeb7
--- /dev/null
+++ b/rockworkd/libpebble/appinfo.cpp
@@ -0,0 +1,163 @@
+#include <QSharedData>
+#include <QBuffer>
+#include <QDir>
+#include <QJsonDocument>
+#include <QUuid>
+#include "appinfo.h"
+#include "watchdatareader.h"
+#include "pebble.h"
+
+namespace {
+struct ResourceEntry {
+ int index;
+ quint32 offset;
+ quint32 length;
+ quint32 crc;
+};
+}
+
+AppInfo::AppInfo(const QString &path):
+ Bundle(path)
+{
+ if (path.isEmpty()) {
+ return;
+ }
+
+ QFile f(path + "/appinfo.json");
+ if (!f.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening appinfo.json";
+ return;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(f.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing appinfo.json";
+ return;
+ }
+
+ m_storeId = path.split("/").last();
+
+ QVariantMap map = jsonDoc.toVariant().toMap();
+
+ m_uuid = map.value("uuid").toUuid();
+ m_shortName = map.value("shortName").toString();
+ m_longName = map.value("longName").toString();
+ m_companyName = map.value("companyName").toString();
+ m_versionCode = map.value("versionCode").toInt();
+ m_versionLabel = map.value("versionLabel").toString();
+ m_capabilities = 0;
+
+ m_isWatchface = map.value("watchapp").toMap().value("watchface").toBool();
+
+ if (map.contains("appKeys")) {
+ QVariantMap appKeyMap = map.value("appKeys").toMap();
+ foreach (const QString &key, appKeyMap.keys()) {
+ m_appKeys.insert(key, appKeyMap.value(key).toInt());
+ }
+ }
+
+ if (map.contains("capabilities")) {
+ QList<QVariant> capabilities = map.value("capabilities").toList();
+
+ foreach (const QVariant &value, capabilities) {
+ QString capability = value.toString();
+ if (capability == "location") {
+ m_capabilities |= Location;
+ }
+ else if (capability == "configurable") {
+ m_capabilities |= Configurable;
+ }
+ }
+ }
+
+ QFile jsApp(path + "/pebble-js-app.js");
+ m_isJsKit = jsApp.exists();
+}
+
+AppInfo::AppInfo(const QUuid &uuid, bool isWatchFace, const QString &name, const QString &vendor, bool hasSettings):
+ m_uuid(uuid),
+ m_shortName(name),
+ m_companyName(vendor),
+ m_capabilities(hasSettings ? Configurable : None),
+ m_isWatchface(isWatchFace),
+ m_isSystemApp(true)
+{
+
+}
+
+
+AppInfo::~AppInfo()
+{}
+
+
+bool AppInfo::isValid() const
+{
+ return !m_uuid.isNull();
+}
+
+QUuid AppInfo::uuid() const
+{
+ return m_uuid;
+}
+
+QString AppInfo::storeId() const
+{
+ return m_storeId;
+}
+
+QString AppInfo::shortName() const
+{
+ return m_shortName;
+}
+
+QString AppInfo::longName() const
+{
+ return m_longName;
+}
+
+QString AppInfo::companyName() const
+{
+ return m_companyName;
+}
+
+int AppInfo::versionCode() const
+{
+ return m_versionCode;
+}
+
+QString AppInfo::versionLabel() const
+{
+ return m_versionLabel;
+}
+
+bool AppInfo::isWatchface() const
+{
+ return m_isWatchface;
+}
+
+bool AppInfo::isJSKit() const
+{
+ return m_isJsKit;
+}
+
+bool AppInfo::isSystemApp() const
+{
+ return m_isSystemApp;
+}
+
+QHash<QString, int> AppInfo::appKeys() const
+{
+ return m_appKeys;
+}
+
+bool AppInfo::hasSettings() const
+{
+ return (m_capabilities & Configurable);
+}
+
+AppInfo::Capabilities AppInfo::capabilities() const
+{
+ return m_capabilities;
+}
+
diff --git a/rockworkd/libpebble/appinfo.h b/rockworkd/libpebble/appinfo.h
new file mode 100644
index 0000000..f3bd256
--- /dev/null
+++ b/rockworkd/libpebble/appinfo.h
@@ -0,0 +1,57 @@
+#ifndef APPINFO_H
+#define APPINFO_H
+
+#include <QUuid>
+#include <QHash>
+#include <QImage>
+#include <QLoggingCategory>
+
+#include "enums.h"
+#include "bundle.h"
+
+class AppInfo: public Bundle
+{
+public:
+ enum Capability {
+ None = 0,
+ Location = 1 << 0,
+ Configurable = 1 << 2
+ };
+ Q_DECLARE_FLAGS(Capabilities, Capability)
+
+ AppInfo(const QString &path = QString());
+ AppInfo(const QUuid &uuid, bool isWatchFace, const QString &name, const QString &vendor, bool hasSettings = false);
+ ~AppInfo();
+
+ bool isValid() const;
+ QUuid uuid() const;
+ QString storeId() const;
+ QString shortName() const;
+ QString longName() const;
+ QString companyName() const;
+ int versionCode() const;
+ QString versionLabel() const;
+ bool isWatchface() const;
+ bool isJSKit() const;
+ bool isSystemApp() const;
+ QHash<QString, int> appKeys() const;
+ Capabilities capabilities() const;
+ bool hasSettings() const;
+
+private:
+ QUuid m_uuid;
+ QString m_storeId;
+ QString m_shortName;
+ QString m_longName;
+ QString m_companyName;
+ int m_versionCode = 0;
+ QString m_versionLabel;
+ QHash<QString, int> m_appKeys;
+ Capabilities m_capabilities;
+
+ bool m_isJsKit = false;
+ bool m_isWatchface = false;
+ bool m_isSystemApp = false;
+};
+
+#endif // APPINFO_H
diff --git a/rockworkd/libpebble/appmanager.cpp b/rockworkd/libpebble/appmanager.cpp
new file mode 100644
index 0000000..04a99c7
--- /dev/null
+++ b/rockworkd/libpebble/appmanager.cpp
@@ -0,0 +1,255 @@
+#include <QDir>
+#include <QSettings>
+
+#include "appmanager.h"
+#include "pebble.h"
+
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "uploadmanager.h"
+
+#include <libintl.h>
+
+#define SETTINGS_APP_UUID "07e0d9cb-8957-4bf7-9d42-35bf47caadfe"
+
+AppManager::AppManager(Pebble *pebble, WatchConnection *connection)
+ : QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ QDir dataDir(m_pebble->storagePath() + "/apps/");
+ if (!dataDir.exists() && !dataDir.mkpath(dataDir.absolutePath())) {
+ qWarning() << "could not create apps dir" << dataDir.absolutePath();
+ }
+ qDebug() << "install apps in" << dataDir.absolutePath();
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointAppFetch, this, "handleAppFetchMessage");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointSorting, this, "sortingReply");
+}
+
+QList<QUuid> AppManager::appUuids() const
+{
+ return m_appList;
+}
+
+//QList<QString> AppManager::appIds() const
+//{
+// return m_appsIds.keys();
+//}
+
+AppInfo AppManager::info(const QUuid &uuid) const
+{
+ return m_apps.value(uuid);
+}
+
+//AppInfo AppManager::info(const QString &id) const
+//{
+// return m_appsUuids.value(m_appsIds.value(id));
+//}
+
+void AppManager::rescan()
+{
+ m_appList.clear();
+ m_apps.clear();
+
+ AppInfo settingsApp(QUuid(SETTINGS_APP_UUID), false, gettext("Settings"), gettext("System app"));
+ m_appList.append(settingsApp.uuid());
+ m_apps.insert(settingsApp.uuid(), settingsApp);
+ AppInfo watchfaces(QUuid("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), false, gettext("Watchfaces"), gettext("System app"));
+ m_appList.append(watchfaces.uuid());
+ m_apps.insert(watchfaces.uuid(), watchfaces);
+ if (m_pebble->capabilities().testFlag(CapabilityHealth)) {
+ AppInfo health(QUuid("36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c"), false, gettext("Health"), gettext("System app"), true);
+ m_appList.append(health.uuid());
+ m_apps.insert(health.uuid(), health);
+ }
+ AppInfo music(QUuid("1f03293d-47af-4f28-b960-f2b02a6dd757"), false, gettext("Music"), gettext("System app"));
+ m_appList.append(music.uuid());
+ m_apps.insert(music.uuid(), music);
+ AppInfo notifications(QUuid("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), false, gettext("Notifications"), gettext("System app"));
+ m_appList.append(notifications.uuid());
+ m_apps.insert(notifications.uuid(), notifications);
+ AppInfo alarms(QUuid("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), false, gettext("Alarms"), gettext("System app"));
+ m_appList.append(alarms.uuid());
+ m_apps.insert(alarms.uuid(), alarms);
+ AppInfo ticToc(QUuid("8f3c8686-31a1-4f5f-91f5-01600c9bdc59"), true, "Tic Toc", gettext("Default watchface"));
+ m_appList.append(ticToc.uuid());
+ m_apps.insert(ticToc.uuid(), ticToc);
+
+ QDir dir(m_pebble->storagePath() + "/apps/");
+ qDebug() << "Scanning Apps dir" << dir.absolutePath();
+ Q_FOREACH(const QString &path, dir.entryList(QDir::Dirs | QDir::Readable)) {
+ QString appPath = dir.absoluteFilePath(path);
+ if (dir.exists(path + "/appinfo.json")) {
+ scanApp(appPath);
+ } else if (QFileInfo(appPath).isFile()) {
+ scanApp(appPath);
+ }
+ }
+
+ QSettings settings(m_pebble->storagePath() + "/apps.conf", QSettings::IniFormat);
+ QStringList storedList = settings.value("appList").toStringList();
+ if (storedList.isEmpty()) {
+ // User did not manually sort the app list yet... We can stop here.
+ return;
+ }
+ // Run some sanity checks
+ if (storedList.count() != m_appList.count()) {
+ qWarning() << "Installed apps not matching order config. App sort order might be wrong.";
+ return;
+ }
+ foreach (const QUuid &uuid, m_appList) {
+ if (!storedList.contains(uuid.toString())) {
+ qWarning() << "Installed apps and stored config order cannot be matched. App sort order might be wrong.";
+ return;
+ }
+ }
+ // All seems fine, repopulate m_appList
+ m_appList.clear();
+ foreach (const QString &storedId, storedList) {
+ m_appList.append(QUuid(storedId));
+ }
+}
+
+void AppManager::handleAppFetchMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ reader.read<quint8>();
+ QUuid uuid = reader.readUuid();
+ quint32 appFetchId = reader.read<quint32>();
+
+ bool haveApp = m_apps.contains(uuid);
+
+ AppFetchResponse response;
+ if (haveApp) {
+ response.setStatus(AppFetchResponse::StatusStart);
+ m_connection->writeToPebble(WatchConnection::EndpointAppFetch, response.serialize());
+ } else {
+ qWarning() << "App with uuid" << uuid.toString() << "which is not installed.";
+ response.setStatus(AppFetchResponse::StatusInvalidUUID);
+ m_connection->writeToPebble(WatchConnection::EndpointAppFetch, response.serialize());
+ emit idMismatchDetected();
+ return;
+ }
+
+ AppInfo appInfo = m_apps.value(uuid);
+
+ QString binaryFile = appInfo.file(AppInfo::FileTypeApplication, m_pebble->hardwarePlatform());
+ quint32 crc = appInfo.crc(AppInfo::FileTypeApplication, m_pebble->hardwarePlatform());
+ qDebug() << "opened binary" << binaryFile << "for hardware" << m_pebble->hardwarePlatform() << "crc" << crc;
+ m_connection->uploadManager()->uploadAppBinary(appFetchId, binaryFile, crc, [this, appInfo, appFetchId](){
+ qDebug() << "binary file uploaded successfully";
+
+ QString resourcesFile = appInfo.file(AppInfo::FileTypeResources, m_pebble->hardwarePlatform());
+ quint32 crc = appInfo.crc(AppInfo::FileTypeResources, m_pebble->hardwarePlatform());
+ qDebug() << "uploadign resource file" << resourcesFile;
+ m_connection->uploadManager()->uploadAppResources(appFetchId, resourcesFile, crc, [this, appInfo, appFetchId]() {
+ qDebug() << "resource file uploaded successfully";
+
+ QString workerFile = appInfo.file(AppInfo::FileTypeWorker, m_pebble->hardwarePlatform());
+ if (!workerFile.isEmpty()) {
+ quint32 crc = appInfo.crc(AppInfo::FileTypeWorker, m_pebble->hardwarePlatform());
+ m_connection->uploadManager()->uploadAppWorker(appFetchId, workerFile, crc, [this]() {
+ qDebug() << "worker file uploaded successfully";
+ });
+ }
+ });
+ });
+}
+
+void AppManager::sortingReply(const QByteArray &data)
+{
+ qDebug() << "have sorting reply" << data.toHex();
+}
+
+void AppManager::insertAppInfo(const AppInfo &info)
+{
+ m_appList.append(info.uuid());
+ m_apps.insert(info.uuid(), info);
+// m_appsIds.insert(info.id(), info.uuid());
+ emit appsChanged();
+}
+
+QUuid AppManager::scanApp(const QString &path)
+{
+ qDebug() << "scanning app" << path;
+ AppInfo info(path);
+ if (info.isValid()) {
+ insertAppInfo(info);
+ }
+ return info.uuid();
+}
+
+void AppManager::removeApp(const QUuid &uuid)
+{
+ m_appList.removeAll(uuid);
+ AppInfo info = m_apps.take(uuid);
+ if (!info.isValid() || info.path().isEmpty()) {
+ qWarning() << "App UUID not found. not removing";
+ return;
+
+ }
+ QDir dir(info.path());
+ dir.removeRecursively();
+ emit appsChanged();
+}
+
+void AppManager::setAppOrder(const QList<QUuid> &newList)
+{
+ // run some sanity checks
+ if (newList.count() != m_appList.count()) {
+ qWarning() << "Number of apps in order list is not matching installed apps.";
+ return;
+ }
+ foreach (const QUuid &installedUuid, m_appList) {
+ if (!newList.contains(installedUuid)) {
+ qWarning() << "App ids in order list not matching with installed apps.";
+ return;
+ }
+ }
+ if (newList.first() != QUuid(SETTINGS_APP_UUID)) {
+ qWarning() << "Settings app must be the first app.";
+ return;
+ }
+
+ m_appList = newList;
+ QSettings settings(m_pebble->storagePath() + "/apps.conf", QSettings::IniFormat);
+ QStringList tmp;
+ foreach (const QUuid &id, m_appList) {
+ tmp << id.toString();
+ }
+ settings.setValue("appList", tmp);
+ emit appsChanged();
+
+ QByteArray data;
+ WatchDataWriter writer(&data);
+ writer.write<quint8>(0x01);
+ writer.write<quint8>(m_appList.count());
+ foreach (const QUuid &uuid, m_appList) {
+ writer.writeUuid(uuid);
+ }
+
+ qDebug() << "writing" << data.toHex();
+ m_connection->writeToPebble(WatchConnection::EndpointSorting, data);
+}
+
+AppFetchResponse::AppFetchResponse(Status status):
+ m_status(status)
+{
+
+}
+
+void AppFetchResponse::setStatus(AppFetchResponse::Status status)
+{
+ m_status = status;
+}
+
+QByteArray AppFetchResponse::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.write<quint8>(m_command);
+ writer.write<quint8>(m_status);
+ return ret;
+}
diff --git a/rockworkd/libpebble/appmanager.h b/rockworkd/libpebble/appmanager.h
new file mode 100644
index 0000000..4766ebc
--- /dev/null
+++ b/rockworkd/libpebble/appmanager.h
@@ -0,0 +1,80 @@
+#ifndef APPMANAGER_H
+#define APPMANAGER_H
+
+#include <QObject>
+#include <QHash>
+#include <QUuid>
+#include "appinfo.h"
+#include "watchconnection.h"
+
+class Pebble;
+
+class AppFetchResponse: public PebblePacket
+{
+public:
+ enum Status {
+ StatusStart = 0x01,
+ StatusBusy = 0x02,
+ StatusInvalidUUID = 0x03,
+ StatusNoData = 0x04
+ };
+ AppFetchResponse(Status status = StatusNoData);
+ void setStatus(Status status);
+
+ QByteArray serialize() const override;
+
+private:
+ quint8 m_command = 1; // I guess there's only one command for now
+ Status m_status = StatusNoData;
+};
+
+class AppManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum Action {
+ ActionGetAppBankStatus = 1,
+ ActionRemoveApp = 2,
+ ActionRefreshApp = 3,
+ ActionGetAppBankUuids = 5
+ };
+
+ explicit AppManager(Pebble *pebble, WatchConnection *connection);
+
+ QList<QUuid> appUuids() const;
+
+ AppInfo info(const QUuid &uuid) const;
+
+ void insertAppInfo(const AppInfo &info);
+
+ QUuid scanApp(const QString &path);
+
+ void removeApp(const QUuid &uuid);
+
+ void setAppOrder(const QList<QUuid> &newList);
+
+public slots:
+ void rescan();
+
+private slots:
+ void handleAppFetchMessage(const QByteArray &data);
+ void sortingReply(const QByteArray &data);
+
+signals:
+ void appsChanged();
+
+ void uploadRequested(const QString &file, quint32 appInstallId);
+
+ void idMismatchDetected();
+
+private:
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ QList<QUuid> m_appList;
+ QHash<QUuid, AppInfo> m_apps;
+};
+
+#endif // APPMANAGER_H
diff --git a/rockworkd/libpebble/appmetadata.cpp b/rockworkd/libpebble/appmetadata.cpp
new file mode 100644
index 0000000..5aa423f
--- /dev/null
+++ b/rockworkd/libpebble/appmetadata.cpp
@@ -0,0 +1,73 @@
+#include "appmetadata.h"
+
+#include "watchdatawriter.h"
+
+AppMetadata::AppMetadata()
+{
+
+}
+
+QUuid AppMetadata::uuid() const
+{
+ return m_uuid;
+}
+
+void AppMetadata::setUuid(const QUuid &uuid)
+{
+ m_uuid = uuid;
+}
+
+void AppMetadata::setFlags(quint32 flags)
+{
+ m_flags = flags;
+}
+
+void AppMetadata::setIcon(quint32 icon)
+{
+ m_icon = icon;
+}
+
+void AppMetadata::setAppVersion(quint8 appVersionMajor, quint8 appVersionMinor)
+{
+ m_appVersionMajor = appVersionMajor;
+ m_appVersionMinor = appVersionMinor;
+}
+
+void AppMetadata::setSDKVersion(quint8 sdkVersionMajor, quint8 sdkVersionMinor)
+{
+ m_sdkVersionMajor = sdkVersionMajor;
+ m_sdkVersionMinor = sdkVersionMinor;
+}
+
+void AppMetadata::setAppFaceBgColor(quint8 color)
+{
+ m_appFaceBgColor = color;
+}
+
+void AppMetadata::setAppFaceTemplateId(quint8 templateId)
+{
+ m_appFaceTemplateId = templateId;
+}
+
+void AppMetadata::setAppName(const QString &appName)
+{
+ m_appName = appName;
+}
+
+QByteArray AppMetadata::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.writeUuid(m_uuid);
+ writer.writeLE<quint32>(m_flags);
+ writer.writeLE<quint32>(m_icon);
+ writer.writeLE<quint8>(m_appVersionMajor);
+ writer.writeLE<quint8>(m_appVersionMinor);
+ writer.writeLE<quint8>(m_sdkVersionMajor);
+ writer.writeLE<quint8>(m_sdkVersionMinor);
+ writer.writeLE<quint8>(m_appFaceBgColor);
+ writer.writeLE<quint8>(m_appFaceTemplateId);
+ writer.writeFixedString(96, m_appName);
+ return ret;
+}
+
diff --git a/rockworkd/libpebble/appmetadata.h b/rockworkd/libpebble/appmetadata.h
new file mode 100644
index 0000000..6583c68
--- /dev/null
+++ b/rockworkd/libpebble/appmetadata.h
@@ -0,0 +1,39 @@
+#ifndef APPMETADATA_H
+#define APPMETADATA_H
+
+#include "watchconnection.h"
+
+class AppMetadata: public PebblePacket
+{
+public:
+ AppMetadata();
+
+ QUuid uuid() const;
+ void setUuid(const QUuid &uuid);
+ void setFlags(quint32 flags);
+ void setIcon(quint32 icon);
+ void setAppVersion(quint8 appVersionMajor, quint8 appVersionMinor);
+ void setSDKVersion(quint8 sdkVersionMajor, quint8 sdkVersionMinor);
+ void setAppFaceBgColor(quint8 color);
+ void setAppFaceTemplateId(quint8 templateId);
+ void setAppName(const QString &appName);
+
+ QByteArray serialize() const;
+signals:
+
+public slots:
+
+private:
+ QUuid m_uuid;
+ quint32 m_flags;
+ quint32 m_icon;
+ quint8 m_appVersionMajor;
+ quint8 m_appVersionMinor;
+ quint8 m_sdkVersionMajor;
+ quint8 m_sdkVersionMinor;
+ quint8 m_appFaceBgColor;
+ quint8 m_appFaceTemplateId;
+ QString m_appName; // fixed, 96
+};
+
+#endif // APPMETADATA_H
diff --git a/rockworkd/libpebble/appmsgmanager.cpp b/rockworkd/libpebble/appmsgmanager.cpp
new file mode 100644
index 0000000..e20c8d0
--- /dev/null
+++ b/rockworkd/libpebble/appmsgmanager.cpp
@@ -0,0 +1,461 @@
+#include <QTimer>
+
+#include "pebble.h"
+#include "appmsgmanager.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+// TODO D-Bus server for non JS kit apps!!!!
+
+AppMsgManager::AppMsgManager(Pebble *pebble, AppManager *apps, WatchConnection *connection)
+ : QObject(pebble),
+ m_pebble(pebble),
+ apps(apps),
+ m_connection(connection), _lastTransactionId(0), _timeout(new QTimer(this))
+{
+ connect(m_connection, &WatchConnection::watchConnected,
+ this, &AppMsgManager::handleWatchConnectedChanged);
+
+ _timeout->setSingleShot(true);
+ _timeout->setInterval(3000);
+ connect(_timeout, &QTimer::timeout,
+ this, &AppMsgManager::handleTimeout);
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointLauncher, this, "handleLauncherMessage");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointAppLaunch, this, "handleAppLaunchMessage");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointApplicationMessage, this, "handleApplicationMessage");
+}
+
+void AppMsgManager::handleLauncherMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint8 messageType = reader.read<quint8>();
+ switch (messageType) {
+ case AppMessagePush:
+ handleLauncherPushMessage(data);
+ break;
+
+ // TODO we ignore those for now.
+ case AppMessageAck:
+ qDebug() << "Watch accepted application launch";
+ break;
+ case AppMessageNack:
+ qDebug() << "Watch denied application launch";
+ break;
+ case AppMessageRequest:
+ qWarning() << "Unhandled Launcher message (AppMessagePush)";
+ break;
+ }
+}
+
+void AppMsgManager::handleApplicationMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint8 messageType = reader.read<quint8>();
+ switch (messageType) {
+ case AppMessagePush:
+ handlePushMessage(data);
+ break;
+ case AppMessageAck:
+ handleAckMessage(data, true);
+ break;
+ case AppMessageNack:
+ handleAckMessage(data, false);
+ break;
+ default:
+ qWarning() << "Unknown application message type:" << int(data.at(0));
+ break;
+ }
+}
+
+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;
+
+ qDebug() << "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)
+{
+ if (m_pebble->softwareVersion() < "v3.0") {
+ WatchConnection::Dict dict;
+ dict.insert(1, LauncherActionStart);
+
+ qDebug() << "Sending start message to launcher" << uuid << dict;
+ QByteArray msg = buildPushMessage(++_lastTransactionId, uuid, dict);
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, msg);
+ }
+ else {
+ QByteArray msg = buildLaunchMessage(LauncherActionStart, uuid);
+ qDebug() << "Sending start message to launcher" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointAppLaunch, msg);
+ }
+}
+
+void AppMsgManager::closeApp(const QUuid &uuid)
+{
+ if (m_pebble->softwareVersion() < "v3.0") {
+ WatchConnection::Dict dict;
+ dict.insert(1, LauncherActionStop);
+
+ qDebug() << "Sending stop message to launcher" << uuid << dict;
+ QByteArray msg = buildPushMessage(++_lastTransactionId, uuid, dict);
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, msg);
+ }
+ else {
+ QByteArray msg = buildLaunchMessage(LauncherActionStop, uuid);
+ qDebug() << "Sending stop message to launcher" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointAppLaunch, msg);
+ }
+}
+
+WatchConnection::Dict AppMsgManager::mapAppKeys(const QUuid &uuid, const QVariantMap &data)
+{
+ AppInfo info = apps->info(uuid);
+ if (info.uuid() != uuid) {
+ qWarning() << "Unknown app GUID while sending message:" << uuid;
+ }
+
+ WatchConnection::Dict d;
+
+ qDebug() << "Have appkeys:" << info.appKeys().keys();
+
+ for (QVariantMap::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) {
+ if (info.appKeys().contains(it.key())) {
+ d.insert(info.appKeys().value(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 {
+ qWarning() << "Unknown appKey" << it.key() << "for app with GUID" << uuid;
+ }
+ }
+ }
+
+ return d;
+}
+
+QVariantMap AppMsgManager::mapAppKeys(const QUuid &uuid, const WatchConnection::Dict &dict)
+{
+ AppInfo info = apps->info(uuid);
+ if (info.uuid() != uuid) {
+ qWarning() << "Unknown app GUID while sending message:" << uuid;
+ }
+
+ QVariantMap data;
+
+ for (WatchConnection::Dict::const_iterator it = dict.constBegin(); it != dict.constEnd(); ++it) {
+ qDebug() << "checking app key" << it.key() << info.appKeys().key(it.key());
+ if (info.appKeys().values().contains(it.key())) {
+ data.insert(info.appKeys().key(it.key()), it.value());
+ } else {
+ qWarning() << "Unknown appKey value" << it.key() << "for app with GUID" << uuid;
+ data.insert(QString::number(it.key()), it.value());
+ }
+ }
+
+ return data;
+}
+
+bool AppMsgManager::unpackAppLaunchMessage(const QByteArray &msg, QUuid *uuid)
+{
+ WatchDataReader reader(msg);
+ quint8 action = reader.read<quint8>();
+ Q_UNUSED(action);
+
+ *uuid = reader.readUuid();
+
+ if (reader.bad()) {
+ return false;
+ }
+
+ return true;
+}
+
+bool AppMsgManager::unpackPushMessage(const QByteArray &msg, quint8 *transaction, QUuid *uuid, WatchConnection::Dict *dict)
+{
+ WatchDataReader reader(msg);
+ quint8 code = reader.read<quint8>();
+ Q_UNUSED(code);
+ Q_ASSERT(code == AppMessagePush);
+
+ *transaction = reader.read<quint8>();
+ *uuid = reader.readUuid();
+ *dict = reader.readDict();
+
+ if (reader.bad()) {
+ return false;
+ }
+
+ return true;
+}
+
+QByteArray AppMsgManager::buildPushMessage(quint8 transaction, const QUuid &uuid, const WatchConnection::Dict &dict)
+{
+ QByteArray ba;
+ WatchDataWriter writer(&ba);
+ writer.write<quint8>(AppMessagePush);
+ writer.write<quint8>(transaction);
+ writer.writeUuid(uuid);
+ writer.writeDict(dict);
+
+ return ba;
+}
+
+QByteArray AppMsgManager::buildLaunchMessage(quint8 messageType, const QUuid &uuid)
+{
+ QByteArray ba;
+ WatchDataWriter writer(&ba);
+ writer.write<quint8>(messageType);
+ writer.writeUuid(uuid);
+
+ return ba;
+}
+
+QByteArray AppMsgManager::buildAckMessage(quint8 transaction)
+{
+ QByteArray ba(2, Qt::Uninitialized);
+ ba[0] = AppMessageAck;
+ ba[1] = transaction;
+ return ba;
+}
+
+QByteArray AppMsgManager::buildNackMessage(quint8 transaction)
+{
+ QByteArray ba(2, Qt::Uninitialized);
+ ba[0] = AppMessageNack;
+ ba[1] = transaction;
+ return ba;
+}
+
+void AppMsgManager::handleAppLaunchMessage(const QByteArray &data)
+{
+ QUuid uuid;
+ if (!unpackAppLaunchMessage(data, &uuid)) {
+ qWarning() << "Failed to parse App Launch message";
+ return;
+ }
+
+ switch (data.at(0)) {
+ case LauncherActionStart:
+ qDebug() << "App starting in watch:" << uuid;
+ emit appStarted(uuid);
+ break;
+ case LauncherActionStop:
+ qDebug() << "App stopping in watch:" << uuid;
+ emit appStopped(uuid);
+ break;
+ default:
+ qWarning() << "App Launch pushed unknown message:" << uuid;
+ break;
+ }
+}
+
+void AppMsgManager::handleLauncherPushMessage(const QByteArray &data)
+{
+ quint8 transaction;
+ QUuid uuid;
+ WatchConnection::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
+ qWarning() << "Failed to parser LAUNCHER PUSH message";
+ return;
+ }
+ qDebug() << "have launcher push message" << data.toHex() << dict.keys();
+ if (!dict.contains(1)) {
+ qWarning() << "LAUNCHER message has no item in dict";
+ return;
+ }
+
+ switch (dict.value(1).toInt()) {
+ case LauncherActionStart:
+ qDebug() << "App starting in watch:" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, buildAckMessage(transaction));
+ emit appStarted(uuid);
+ break;
+ case LauncherActionStop:
+ qDebug() << "App stopping in watch:" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, buildAckMessage(transaction));
+ emit appStopped(uuid);
+ break;
+ default:
+ qWarning() << "LAUNCHER pushed unknown message:" << uuid << dict;
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, buildNackMessage(transaction));
+ break;
+ }
+}
+
+void AppMsgManager::handlePushMessage(const QByteArray &data)
+{
+ quint8 transaction;
+ QUuid uuid;
+ WatchConnection::Dict dict;
+
+ if (!unpackPushMessage(data, &transaction, &uuid, &dict)) {
+ qWarning() << "Failed to parse APP_MSG PUSH";
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, buildNackMessage(transaction));
+ return;
+ }
+
+ qDebug() << "Received appmsg PUSH from" << uuid << "with" << dict;
+
+ QVariantMap msg = mapAppKeys(uuid, dict);
+ qDebug() << "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) {
+ qDebug() << "ACKing transaction" << transaction;
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, buildAckMessage(transaction));
+ } else {
+ qDebug() << "NACKing transaction" << transaction;
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, buildNackMessage(transaction));
+ }
+}
+
+void AppMsgManager::handleAckMessage(const QByteArray &data, bool ack)
+{
+ if (data.size() < 2) {
+ qWarning() << "invalid ack/nack message size";
+ return;
+ }
+
+ const quint8 type = data[0]; Q_UNUSED(type);
+ const quint8 recv_transaction = data[1];
+
+ Q_ASSERT(type == AppMessageAck || type == AppMessageNack);
+
+ if (_pending.empty()) {
+ qWarning() << "received an ack/nack for transaction" << recv_transaction << "but no transaction is pending";
+ return;
+ }
+
+ PendingTransaction &trans = _pending.head();
+ if (trans.transactionId != recv_transaction) {
+ qWarning() << "received an ack/nack but for the wrong transaction";
+ }
+
+ qDebug() << "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 (!m_connection->isConnected()) {
+ abortPendingTransactions();
+ }
+}
+
+void AppMsgManager::handleTimeout()
+{
+ // Abort the first transaction
+ Q_ASSERT(!_pending.empty());
+ PendingTransaction trans = _pending.dequeue();
+
+ qWarning() << "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);
+
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, 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/rockworkd/libpebble/appmsgmanager.h b/rockworkd/libpebble/appmsgmanager.h
new file mode 100644
index 0000000..77ee480
--- /dev/null
+++ b/rockworkd/libpebble/appmsgmanager.h
@@ -0,0 +1,94 @@
+#ifndef APPMSGMANAGER_H
+#define APPMSGMANAGER_H
+
+#include <functional>
+#include <QUuid>
+#include <QQueue>
+
+#include "watchconnection.h"
+#include "appmanager.h"
+
+class AppMsgManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum AppMessage {
+ AppMessagePush = 1,
+ AppMessageRequest = 2,
+ AppMessageAck = 0xFF,
+ AppMessageNack = 0x7F
+ };
+ enum LauncherMessage {
+ LauncherActionStart = 1,
+ LauncherActionStop = 0
+ };
+
+ explicit AppMsgManager(Pebble *pebble, AppManager *apps, WatchConnection *connection);
+
+ 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:
+ WatchConnection::Dict mapAppKeys(const QUuid &uuid, const QVariantMap &data);
+ QVariantMap mapAppKeys(const QUuid &uuid, const WatchConnection::Dict &dict);
+
+ static bool unpackAppLaunchMessage(const QByteArray &msg, QUuid *uuid);
+ static bool unpackPushMessage(const QByteArray &msg, quint8 *transaction, QUuid *uuid, WatchConnection::Dict *dict);
+
+ static QByteArray buildPushMessage(quint8 transaction, const QUuid &uuid, const WatchConnection::Dict &dict);
+ static QByteArray buildLaunchMessage(quint8 messageType, const QUuid &uuid);
+ 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();
+
+ void handleAppLaunchMessage(const QByteArray &data);
+ void handleLauncherMessage(const QByteArray &data);
+ void handleApplicationMessage(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ AppManager *apps;
+ WatchConnection *m_connection;
+ QHash<QUuid, MessageHandlerFunc> _handlers;
+ quint8 _lastTransactionId;
+
+ struct PendingTransaction {
+ quint8 transactionId;
+ QUuid uuid;
+ WatchConnection::Dict dict;
+ std::function<void()> ackCallback;
+ std::function<void()> nackCallback;
+ };
+ QQueue<PendingTransaction> _pending;
+ QTimer *_timeout;
+};
+
+#endif // APPMSGMANAGER_H
diff --git a/rockworkd/libpebble/blobdb.cpp b/rockworkd/libpebble/blobdb.cpp
new file mode 100644
index 0000000..e5a2f77
--- /dev/null
+++ b/rockworkd/libpebble/blobdb.cpp
@@ -0,0 +1,584 @@
+#include "blobdb.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+#include <QDebug>
+#include <QOrganizerRecurrenceRule>
+#include <QDir>
+#include <QSettings>
+
+BlobDB::BlobDB(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointBlobDB, this, "blobCommandReply");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointActionHandler, this, "actionInvoked");
+
+ connect(m_connection, &WatchConnection::watchConnected, [this]() {
+ if (m_currentCommand) {
+ delete m_currentCommand;
+ m_currentCommand = nullptr;
+ }
+ });
+
+ m_blobDBStoragePath = m_pebble->storagePath() + "/blobdb/";
+ QDir dir(m_blobDBStoragePath);
+ if (!dir.exists() && !dir.mkpath(m_blobDBStoragePath)) {
+ qWarning() << "Error creating blobdb storage dir.";
+ return;
+ }
+ dir.setNameFilters({"calendarevent-*"});
+ foreach (const QFileInfo &fi, dir.entryInfoList()) {
+ CalendarEvent event;
+ event.loadFromCache(m_blobDBStoragePath, fi.fileName().right(QUuid().toString().length()));
+
+ m_calendarEntries.append(event);
+ }
+}
+
+void BlobDB::insertNotification(const Notification &notification)
+{
+ TimelineAttribute::IconID iconId = TimelineAttribute::IconIDDefaultBell;
+ TimelineAttribute::Color color = TimelineAttribute::ColorRed;
+ QString muteName;
+ switch (notification.type()) {
+ case Notification::NotificationTypeAlarm:
+ iconId = TimelineAttribute::IconIDAlarm;
+ muteName = "Alarms";
+ break;
+ case Notification::NotificationTypeFacebook:
+ iconId = TimelineAttribute::IconIDFacebook;
+ color = TimelineAttribute::ColorBlue;
+ muteName = "facebook";
+ break;
+ case Notification::NotificationTypeGMail:
+ iconId = TimelineAttribute::IconIDGMail;
+ muteName = "GMail";
+ break;
+ case Notification::NotificationTypeHangout:
+ iconId = TimelineAttribute::IconIDHangout;
+ color = TimelineAttribute::ColorGreen;
+ muteName = "Hangout";
+ break;
+ case Notification::NotificationTypeMissedCall:
+ iconId = TimelineAttribute::IconIDDefaultMissedCall;
+ muteName = "call notifications";
+ break;
+ case Notification::NotificationTypeMusic:
+ iconId = TimelineAttribute::IconIDMusic;
+ muteName = "music";
+ break;
+ case Notification::NotificationTypeReminder:
+ iconId = TimelineAttribute::IconIDReminder;
+ muteName = "reminders";
+ break;
+ case Notification::NotificationTypeTelegram:
+ iconId = TimelineAttribute::IconIDTelegram;
+ color = TimelineAttribute::ColorLightBlue;
+ muteName = "Telegram";
+ break;
+ case Notification::NotificationTypeTwitter:
+ iconId = TimelineAttribute::IconIDTwitter;
+ color = TimelineAttribute::ColorBlue2;
+ muteName = "Twitter";
+ break;
+ case Notification::NotificationTypeWeather:
+ iconId = TimelineAttribute::IconIDWeather;
+ muteName = "Weather";
+ break;
+ case Notification::NotificationTypeWhatsApp:
+ iconId = TimelineAttribute::IconIDWhatsApp;
+ color = TimelineAttribute::ColorGreen;
+ muteName = "WhatsApp";
+ break;
+ case Notification::NotificationTypeSMS:
+ muteName = "SMS";
+ iconId = TimelineAttribute::IconIDDefaultBell;
+ break;
+ case Notification::NotificationTypeEmail:
+ default:
+ muteName = "e mails";
+ iconId = TimelineAttribute::IconIDDefaultBell;
+ break;
+ }
+
+ QUuid itemUuid = QUuid::createUuid();
+ TimelineItem timelineItem(itemUuid, TimelineItem::TypeNotification);
+ timelineItem.setFlags(TimelineItem::FlagSingleEvent);
+
+ TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, notification.sender().left(64).toUtf8());
+ timelineItem.appendAttribute(titleAttribute);
+
+ TimelineAttribute subjectAttribute(TimelineAttribute::TypeSubtitle, notification.subject().left(64).toUtf8());
+ timelineItem.appendAttribute(subjectAttribute);
+
+ TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, notification.body().toUtf8());
+ timelineItem.appendAttribute(bodyAttribute);
+
+ TimelineAttribute iconAttribute(TimelineAttribute::TypeTinyIcon, iconId);
+ timelineItem.appendAttribute(iconAttribute);
+
+ TimelineAttribute colorAttribute(TimelineAttribute::TypeColor, color);
+ timelineItem.appendAttribute(colorAttribute);
+
+ TimelineAction dismissAction(0, TimelineAction::TypeDismiss);
+ TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss");
+ dismissAction.appendAttribute(dismissAttribute);
+ timelineItem.appendAction(dismissAction);
+
+ TimelineAction muteAction(1, TimelineAction::TypeGeneric);
+ TimelineAttribute muteActionAttribute(TimelineAttribute::TypeTitle, "Mute " + muteName.toUtf8());
+ muteAction.appendAttribute(muteActionAttribute);
+ timelineItem.appendAction(muteAction);
+
+ if (!notification.actToken().isEmpty()) {
+ TimelineAction actAction(2, TimelineAction::TypeGeneric);
+ TimelineAttribute actActionAttribute(TimelineAttribute::TypeTitle, "Open on phone");
+ actAction.appendAttribute(actActionAttribute);
+ timelineItem.appendAction(actAction);
+ }
+
+ insert(BlobDB::BlobDBIdNotification, timelineItem);
+ m_notificationSources.insert(itemUuid, notification);
+}
+
+void BlobDB::insertTimelinePin(const QUuid &uuid, TimelineItem::Layout layout, const QDateTime &startTime, const QDateTime &endTime, const QString &title, const QString &desctiption, const QMap<QString, QString> fields, bool recurring)
+{
+// TimelineItem item(TimelineItem::TypePin, TimelineItem::FlagSingleEvent, QDateTime::currentDateTime().addMSecs(1000 * 60 * 2), 60);
+
+ qDebug() << "inserting timeline pin:" << title << startTime << endTime;
+ int duration = (endTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch()) / 1000 / 60;
+ TimelineItem item(uuid, TimelineItem::TypePin, TimelineItem::FlagSingleEvent, startTime, duration);
+ item.setLayout(layout);
+
+ TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, title.toUtf8());
+ item.appendAttribute(titleAttribute);
+
+ if (!desctiption.isEmpty()) {
+ TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, desctiption.left(128).toUtf8());
+ item.appendAttribute(bodyAttribute);
+ }
+
+// TimelineAttribute iconAttribute(TimelineAttribute::TypeTinyIcon, TimelineAttribute::IconIDTelegram);
+// item.appendAttribute(iconAttribute);
+
+ if (!fields.isEmpty()) {
+ TimelineAttribute fieldNames(TimelineAttribute::TypeFieldNames, fields.keys());
+ item.appendAttribute(fieldNames);
+
+ TimelineAttribute fieldValues(TimelineAttribute::TypeFieldValues, fields.values());
+ item.appendAttribute(fieldValues);
+ }
+
+ if (recurring) {
+ TimelineAttribute guess(TimelineAttribute::TypeRecurring, 0x01);
+ item.appendAttribute(guess);
+ }
+
+ TimelineAction dismissAction(0, TimelineAction::TypeDismiss);
+ TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss");
+ dismissAction.appendAttribute(dismissAttribute);
+ item.appendAction(dismissAction);
+
+ insert(BlobDB::BlobDBIdPin, item);
+}
+
+void BlobDB::removeTimelinePin(const QUuid &uuid)
+{
+ qDebug() << "Removing timeline pin:" << uuid;
+ remove(BlobDBId::BlobDBIdPin, uuid);
+}
+
+void BlobDB::insertReminder()
+{
+
+ TimelineItem item(TimelineItem::TypeReminder, TimelineItem::FlagSingleEvent, QDateTime::currentDateTime().addMSecs(1000 * 60 * 2), 0);
+
+ TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, "ReminderTitle");
+ item.appendAttribute(titleAttribute);
+
+ TimelineAttribute subjectAttribute(TimelineAttribute::TypeSubtitle, "ReminderSubtitle");
+ item.appendAttribute(subjectAttribute);
+
+ TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, "ReminderBody");
+ item.appendAttribute(bodyAttribute);
+
+ QByteArray data;
+ data.append(0x07); data.append('\0'); data.append('\0'); data.append(0x80);
+ TimelineAttribute guessAttribute(TimelineAttribute::TypeTinyIcon, data);
+ item.appendAttribute(guessAttribute);
+ qDebug() << "attrib" << guessAttribute.serialize();
+
+ TimelineAction dismissAction(0, TimelineAction::TypeDismiss);
+ TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss");
+ dismissAction.appendAttribute(dismissAttribute);
+ item.appendAction(dismissAction);
+
+ insert(BlobDB::BlobDBIdReminder, item);
+ // qDebug() << "adding timeline item" << ddd.toHex();
+
+}
+
+void BlobDB::clearTimeline()
+{
+ foreach (CalendarEvent entry, m_calendarEntries) {
+ entry.removeFromCache(m_blobDBStoragePath);
+ }
+ m_calendarEntries.clear();
+ clear(BlobDB::BlobDBIdPin);
+}
+
+void BlobDB::syncCalendar(const QList<CalendarEvent> &events)
+{
+ qDebug() << "BlobDB: Starting calendar sync for" << events.count() << "entries";
+ QList<CalendarEvent> itemsToSync;
+ QList<CalendarEvent> itemsToAdd;
+ QList<CalendarEvent> itemsToDelete;
+
+ // Filter out invalid items
+ foreach (const CalendarEvent &event, events) {
+ if (event.startTime().isValid() && event.endTime().isValid()
+ && event.startTime().addDays(2) > QDateTime::currentDateTime()
+ && QDateTime::currentDateTime().addDays(5) > event.startTime()) {
+ itemsToSync.append(event);
+ }
+ }
+
+ // Compare events to local ones
+ foreach (const CalendarEvent &event, itemsToSync) {
+ CalendarEvent syncedEvent = findCalendarEvent(event.id());
+ if (!syncedEvent.isValid()) {
+ itemsToAdd.append(event);
+ } else if (!(syncedEvent == event)) {
+ qDebug() << "event has changed!";
+ itemsToDelete.append(syncedEvent);
+ itemsToAdd.append(event);
+ }
+ }
+
+ // Find stale local ones
+ foreach (const CalendarEvent &event, m_calendarEntries) {
+ bool found = false;
+ foreach (const CalendarEvent &tmp, events) {
+ if (tmp.id() == event.id()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ qDebug() << "removing stale timeline entry";
+ itemsToDelete.append(event);
+ }
+ }
+
+ foreach (const CalendarEvent &event, itemsToDelete) {
+ removeTimelinePin(event.uuid());
+ m_calendarEntries.removeAll(event);
+ event.removeFromCache(m_blobDBStoragePath);
+ }
+
+ qDebug() << "adding" << itemsToAdd.count() << "timeline entries";
+ foreach (const CalendarEvent &event, itemsToAdd) {
+ QMap<QString, QString> fields;
+ if (!event.location().isEmpty()) fields.insert("Location", event.location());
+ if (!event.calendar().isEmpty()) fields.insert("Calendar", event.calendar());
+ if (!event.comment().isEmpty()) fields.insert("Comments", event.comment());
+ if (!event.guests().isEmpty()) fields.insert("Guests", event.guests().join(", "));
+ insertTimelinePin(event.uuid(), TimelineItem::LayoutCalendar, event.startTime(), event.endTime(), event.title(), event.description(), fields, event.recurring());
+ m_calendarEntries.append(event);
+ event.saveToCache(m_blobDBStoragePath);
+ }
+}
+
+void BlobDB::clearApps()
+{
+ clear(BlobDBId::BlobDBIdApp);
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ s.remove("");
+}
+
+void BlobDB::insertAppMetaData(const AppInfo &info)
+{
+ if (!m_pebble->connected()) {
+ qWarning() << "Pebble is not connected. Cannot install app";
+ return;
+ }
+
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ if (s.value(info.uuid().toString(), false).toBool()) {
+ qWarning() << "App already in DB. Not syncing again";
+ return;
+ }
+
+ AppMetadata metaData = appInfoToMetadata(info, m_pebble->hardwarePlatform());
+
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = BlobDBIdApp;
+
+ cmd->m_key = metaData.uuid().toRfc4122();
+ cmd->m_value = metaData.serialize();
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::removeApp(const AppInfo &info)
+{
+ remove(BlobDBId::BlobDBIdApp, info.uuid());
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ s.remove(info.uuid().toString());
+}
+
+void BlobDB::insert(BlobDBId database, const TimelineItem &item)
+{
+ if (!m_connection->isConnected()) {
+ return;
+ }
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = database;
+
+ cmd->m_key = item.itemId().toRfc4122();
+ cmd->m_value = item.serialize();
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::remove(BlobDB::BlobDBId database, const QUuid &uuid)
+{
+ if (!m_connection->isConnected()) {
+ return;
+ }
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationDelete;
+ cmd->m_token = generateToken();
+ cmd->m_database = database;
+
+ cmd->m_key = uuid.toRfc4122();
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::clear(BlobDB::BlobDBId database)
+{
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationClear;
+ cmd->m_token = generateToken();
+ cmd->m_database = database;
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::setHealthParams(const HealthParams &healthParams)
+{
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = BlobDBIdAppSettings;
+
+ cmd->m_key = "activityPreferences";
+ cmd->m_value = healthParams.serialize();
+
+ qDebug() << "Setting health params. Enabled:" << healthParams.enabled() << cmd->serialize().toHex();
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::setUnits(bool imperial)
+{
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = BlobDBIdAppSettings;
+
+ cmd->m_key = "unitsDistance";
+ WatchDataWriter writer(&cmd->m_value);
+ writer.write<quint8>(imperial ? 0x01 : 0x00);
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::blobCommandReply(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint16 token = reader.readLE<quint16>();
+ quint8 status = reader.read<quint8>();
+ if (m_currentCommand->m_token != token) {
+ qWarning() << "Received reply for unexpected token";
+ } else if (status != 0x01) {
+ qWarning() << "Blob Command failed:" << status;
+ } else { // All is well
+ if (m_currentCommand->m_database == BlobDBIdApp && m_currentCommand->m_command == OperationInsert) {
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ QUuid appUuid = QUuid::fromRfc4122(m_currentCommand->m_key);
+ s.setValue(appUuid.toString(), true);
+ emit appInserted(appUuid);
+ }
+ }
+
+ if (m_currentCommand && token == m_currentCommand->m_token) {
+ delete m_currentCommand;
+ m_currentCommand = nullptr;
+ sendNext();
+ }
+}
+
+void BlobDB::actionInvoked(const QByteArray &actionReply)
+{
+ WatchDataReader reader(actionReply);
+ TimelineAction::Type actionType = (TimelineAction::Type)reader.read<quint8>();
+ QUuid notificationId = reader.readUuid();
+ quint8 actionId = reader.read<quint8>();
+ quint8 param = reader.read<quint8>(); // Is this correct? So far I've only seen 0x00 in here
+
+ // Not sure what to do with those yet
+ Q_UNUSED(actionType)
+ Q_UNUSED(param)
+
+ qDebug() << "Action invoked" << actionId << actionReply.toHex();
+
+ Status status = StatusError;
+ QList<TimelineAttribute> attributes;
+
+ Notification notification = m_notificationSources.value(notificationId);
+ QString sourceId = notification.sourceId();
+ if (sourceId.isEmpty()) {
+ status = StatusError;
+ } else {
+ switch (actionId) {
+ case 1: { // Mute source
+ TimelineAttribute textAttribute(TimelineAttribute::TypeSubtitle, "Muted!");
+ attributes.append(textAttribute);
+// TimelineAttribute iconAttribute(TimelineAttribute::TypeLargeIcon, TimelineAttribute::IconIDTelegram);
+// attributes.append(iconAttribute);
+ emit muteSource(sourceId);
+ status = StatusSuccess;
+ break;
+ }
+ case 2: { // Open on phone
+ TimelineAttribute textAttribute(TimelineAttribute::TypeSubtitle, "Opened!");
+ attributes.append(textAttribute);
+ qDebug() << "opening" << notification.actToken();
+ emit actionTriggered(notification.actToken());
+ status = StatusSuccess;
+ }
+ }
+ }
+
+ QByteArray reply;
+ reply.append(0x11); // Length of id & status code
+ reply.append(notificationId.toRfc4122());
+ reply.append(status);
+ reply.append(attributes.count());
+ foreach (const TimelineAttribute &attrib, attributes) {
+ reply.append(attrib.serialize());
+ }
+ m_connection->writeToPebble(WatchConnection::EndpointActionHandler, reply);
+}
+
+void BlobDB::sendActionReply()
+{
+
+}
+
+void BlobDB::sendNext()
+{
+ if (m_currentCommand || m_commandQueue.isEmpty()) {
+ return;
+ }
+ m_currentCommand = m_commandQueue.takeFirst();
+ m_connection->writeToPebble(WatchConnection::EndpointBlobDB, m_currentCommand->serialize());
+}
+
+quint16 BlobDB::generateToken()
+{
+ return (qrand() % ((int)pow(2, 16) - 2)) + 1;
+}
+
+AppMetadata BlobDB::appInfoToMetadata(const AppInfo &info, HardwarePlatform hardwarePlatform)
+{
+ QString binaryFile = info.file(AppInfo::FileTypeApplication, hardwarePlatform);
+ QFile f(binaryFile);
+ if (!f.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening app binary";
+ return AppMetadata();
+ }
+ QByteArray data = f.read(512);
+ WatchDataReader reader(data);
+ qDebug() << "Header:" << reader.readFixedString(8);
+ qDebug() << "struct Major version:" << reader.read<quint8>();
+ qDebug() << "struct Minor version:" << reader.read<quint8>();
+ quint8 sdkVersionMajor = reader.read<quint8>();
+ qDebug() << "sdk Major version:" << sdkVersionMajor;
+ quint8 sdkVersionMinor = reader.read<quint8>();
+ qDebug() << "sdk Minor version:" << sdkVersionMinor;
+ quint8 appVersionMajor = reader.read<quint8>();
+ qDebug() << "app Major version:" << appVersionMajor;
+ quint8 appVersionMinor = reader.read<quint8>();
+ qDebug() << "app Minor version:" << appVersionMinor;
+ qDebug() << "size:" << reader.readLE<quint16>();
+ qDebug() << "offset:" << reader.readLE<quint32>();
+ qDebug() << "crc:" << reader.readLE<quint32>();
+ QString appName = reader.readFixedString(32);
+ qDebug() << "App name:" << appName;
+ qDebug() << "Vendor name:" << reader.readFixedString(32);
+ quint32 icon = reader.readLE<quint32>();
+ qDebug() << "Icon:" << icon;
+ qDebug() << "Symbol table address:" << reader.readLE<quint32>();
+ quint32 flags = reader.readLE<quint32>();
+ qDebug() << "Flags:" << flags;
+ qDebug() << "Num relocatable entries:" << reader.readLE<quint32>();
+
+ f.close();
+ qDebug() << "app data" << data.toHex();
+
+ AppMetadata metadata;
+ metadata.setUuid(info.uuid());
+ metadata.setFlags(flags);
+ metadata.setAppVersion(appVersionMajor, appVersionMinor);
+ metadata.setSDKVersion(sdkVersionMajor, sdkVersionMinor);
+ metadata.setAppFaceBgColor(0);
+ metadata.setAppFaceTemplateId(0);
+ metadata.setAppName(appName);
+ metadata.setIcon(icon);
+ return metadata;
+
+}
+
+CalendarEvent BlobDB::findCalendarEvent(const QString &id)
+{
+ foreach (const CalendarEvent &entry, m_calendarEntries) {
+ if (entry.id() == id) {
+ return entry;
+ }
+ }
+ return CalendarEvent();
+}
+
+QByteArray BlobDB::BlobCommand::serialize() const
+{
+ QByteArray ret;
+ ret.append((quint8)m_command);
+ ret.append(m_token & 0xFF); ret.append(((m_token >> 8) & 0xFF));
+ ret.append((quint8)m_database);
+
+ if (m_command == BlobDB::OperationInsert || m_command == BlobDB::OperationDelete) {
+ ret.append(m_key.length() & 0xFF);
+ ret.append(m_key);
+ }
+ if (m_command == BlobDB::OperationInsert) {
+ ret.append(m_value.length() & 0xFF); ret.append((m_value.length() >> 8) & 0xFF); // value length
+ ret.append(m_value);
+ }
+
+ return ret;
+}
diff --git a/rockworkd/libpebble/blobdb.h b/rockworkd/libpebble/blobdb.h
new file mode 100644
index 0000000..b1db403
--- /dev/null
+++ b/rockworkd/libpebble/blobdb.h
@@ -0,0 +1,108 @@
+#ifndef BLOBDB_H
+#define BLOBDB_H
+
+#include "watchconnection.h"
+#include "pebble.h"
+#include "timelineitem.h"
+#include "healthparams.h"
+#include "appmetadata.h"
+
+#include <QObject>
+#include <QDateTime>
+#include <QOrganizerEvent>
+
+QTORGANIZER_USE_NAMESPACE
+
+
+class BlobDB : public QObject
+{
+ Q_OBJECT
+public:
+ enum BlobDBId {
+ BlobDBIdTest = 0,
+ BlobDBIdPin = 1,
+ BlobDBIdApp = 2,
+ BlobDBIdReminder = 3,
+ BlobDBIdNotification = 4,
+ BlobDBIdAppSettings = 7
+
+ };
+ enum Operation {
+ OperationInsert = 0x01,
+ OperationDelete = 0x04,
+ OperationClear = 0x05
+ };
+
+ enum Status {
+ StatusSuccess = 0x00,
+ StatusError = 0x01
+ };
+
+
+ explicit BlobDB(Pebble *pebble, WatchConnection *connection);
+
+ void insertNotification(const Notification &notification);
+ void insertTimelinePin(const QUuid &uuid, TimelineItem::Layout layout, const QDateTime &startTime, const QDateTime &endTime, const QString &title, const QString &desctiption, const QMap<QString, QString> fields, bool recurring);
+ void removeTimelinePin(const QUuid &uuid);
+ void insertReminder();
+ void clearTimeline();
+ void syncCalendar(const QList<CalendarEvent> &events);
+
+ void clearApps();
+ void insertAppMetaData(const AppInfo &info);
+ void removeApp(const AppInfo &info);
+
+ void insert(BlobDBId database, const TimelineItem &item);
+ void remove(BlobDBId database, const QUuid &uuid);
+ void clear(BlobDBId database);
+
+ void setHealthParams(const HealthParams &healthParams);
+ void setUnits(bool imperial);
+
+private slots:
+ void blobCommandReply(const QByteArray &data);
+ void actionInvoked(const QByteArray &data);
+ void sendActionReply();
+ void sendNext();
+
+signals:
+ void muteSource(const QString &sourceId);
+ void actionTriggered(const QString &actToken);
+ void appInserted(const QUuid &uuid);
+
+private:
+ quint16 generateToken();
+ AppMetadata appInfoToMetadata(const AppInfo &info, HardwarePlatform hardwarePlatform);
+
+private:
+
+ class BlobCommand: public PebblePacket
+ {
+ public:
+ BlobDB::Operation m_command; // quint8
+ quint16 m_token;
+ BlobDB::BlobDBId m_database;
+
+ QByteArray m_key;
+ QByteArray m_value;
+
+ QByteArray serialize() const override;
+ };
+
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+
+ QHash<QUuid, Notification> m_notificationSources;
+
+ QList<CalendarEvent> m_calendarEntries;
+ CalendarEvent findCalendarEvent(const QString &id);
+
+ HealthParams m_healthParams;
+
+ BlobCommand *m_currentCommand = nullptr;
+ QList<BlobCommand*> m_commandQueue;
+
+ QString m_blobDBStoragePath;
+};
+
+#endif // BLOBDB_H
diff --git a/rockworkd/libpebble/bluez/bluez_adapter1.cpp b/rockworkd/libpebble/bluez/bluez_adapter1.cpp
new file mode 100644
index 0000000..a386af1
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_adapter1.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAdapter1 -p bluez_adapter1 -v org.bluez.Adapter1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 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.
+ */
+
+#include "bluez_adapter1.h"
+
+/*
+ * Implementation of interface class BluezAdapter1
+ */
+
+BluezAdapter1::BluezAdapter1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+BluezAdapter1::~BluezAdapter1()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/bluez_adapter1.h b/rockworkd/libpebble/bluez/bluez_adapter1.h
new file mode 100644
index 0000000..8690075
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_adapter1.h
@@ -0,0 +1,66 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAdapter1 -p bluez_adapter1 -v org.bluez.Adapter1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef BLUEZ_ADAPTER1_H_1442480417
+#define BLUEZ_ADAPTER1_H_1442480417
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.bluez.Adapter1
+ */
+class BluezAdapter1: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.bluez.Adapter1"; }
+
+public:
+ BluezAdapter1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~BluezAdapter1();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<> RemoveDevice(const QDBusObjectPath &device)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(device);
+ return asyncCallWithArgumentList(QStringLiteral("RemoveDevice"), argumentList);
+ }
+
+ inline QDBusPendingReply<> StartDiscovery()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("StartDiscovery"), argumentList);
+ }
+
+ inline QDBusPendingReply<> StopDiscovery()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("StopDiscovery"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+};
+
+namespace org {
+ namespace bluez {
+ typedef ::BluezAdapter1 Adapter1;
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp b/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp
new file mode 100644
index 0000000..630953b
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAgentManager1 -p bluez_agentmanager1 org.bluez.AgentManager1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 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.
+ */
+
+#include "bluez_agentmanager1.h"
+
+/*
+ * Implementation of interface class BluezAgentManager1
+ */
+
+BluezAgentManager1::BluezAgentManager1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+BluezAgentManager1::~BluezAgentManager1()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/bluez_agentmanager1.h b/rockworkd/libpebble/bluez/bluez_agentmanager1.h
new file mode 100644
index 0000000..5f50e0d
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_agentmanager1.h
@@ -0,0 +1,68 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAgentManager1 -p bluez_agentmanager1 org.bluez.AgentManager1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef BLUEZ_AGENTMANAGER1_H_1442489332
+#define BLUEZ_AGENTMANAGER1_H_1442489332
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.bluez.AgentManager1
+ */
+class BluezAgentManager1: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.bluez.AgentManager1"; }
+
+public:
+ BluezAgentManager1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~BluezAgentManager1();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<> RegisterAgent(const QDBusObjectPath &agent, const QString &capability)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(agent) << QVariant::fromValue(capability);
+ return asyncCallWithArgumentList(QStringLiteral("RegisterAgent"), argumentList);
+ }
+
+ inline QDBusPendingReply<> RequestDefaultAgent(const QDBusObjectPath &agent)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(agent);
+ return asyncCallWithArgumentList(QStringLiteral("RequestDefaultAgent"), argumentList);
+ }
+
+ inline QDBusPendingReply<> UnregisterAgent(const QDBusObjectPath &agent)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(agent);
+ return asyncCallWithArgumentList(QStringLiteral("UnregisterAgent"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+};
+
+namespace org {
+ namespace bluez {
+ typedef ::BluezAgentManager1 AgentManager1;
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/bluez_device1.cpp b/rockworkd/libpebble/bluez/bluez_device1.cpp
new file mode 100644
index 0000000..b5ee0f8
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_device1.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezDevice1 -p bluez_device1 org.bluez.Device1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 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.
+ */
+
+#include "bluez_device1.h"
+
+/*
+ * Implementation of interface class BluezDevice1
+ */
+
+BluezDevice1::BluezDevice1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+BluezDevice1::~BluezDevice1()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/bluez_device1.h b/rockworkd/libpebble/bluez/bluez_device1.h
new file mode 100644
index 0000000..c9eaa1f
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_device1.h
@@ -0,0 +1,85 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezDevice1 -p bluez_device1 org.bluez.Device1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef BLUEZ_DEVICE1_H_1442480478
+#define BLUEZ_DEVICE1_H_1442480478
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.bluez.Device1
+ */
+class BluezDevice1: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.bluez.Device1"; }
+
+public:
+ BluezDevice1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~BluezDevice1();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<> CancelPairing()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("CancelPairing"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Connect()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("Connect"), argumentList);
+ }
+
+ inline QDBusPendingReply<> ConnectProfile(const QString &UUID)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(UUID);
+ return asyncCallWithArgumentList(QStringLiteral("ConnectProfile"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Disconnect()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("Disconnect"), argumentList);
+ }
+
+ inline QDBusPendingReply<> DisconnectProfile(const QString &UUID)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(UUID);
+ return asyncCallWithArgumentList(QStringLiteral("DisconnectProfile"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Pair()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("Pair"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+};
+
+namespace org {
+ namespace bluez {
+ typedef ::BluezDevice1 Device1;
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/bluez_helper.h b/rockworkd/libpebble/bluez/bluez_helper.h
new file mode 100644
index 0000000..363f7ae
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_helper.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+*/
+
+#ifndef BLUEZ_HELPER_H_
+#define BLUEZ_HELPER_H_
+
+#include <QObject>
+#include <QDBusObjectPath>
+
+typedef QMap<QString, QVariantMap> InterfaceList;
+typedef QMap<QDBusObjectPath, InterfaceList> ManagedObjectList;
+
+Q_DECLARE_METATYPE(InterfaceList)
+Q_DECLARE_METATYPE(ManagedObjectList)
+
+#endif
diff --git a/rockworkd/libpebble/bluez/bluezclient.cpp b/rockworkd/libpebble/bluez/bluezclient.cpp
new file mode 100644
index 0000000..8cdf848
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluezclient.cpp
@@ -0,0 +1,84 @@
+#include "bluezclient.h"
+#include "dbus-shared.h"
+
+#include <QDBusConnection>
+#include <QDBusReply>
+#include <QDebug>
+
+BluezClient::BluezClient(QObject *parent):
+ QObject(parent),
+ m_dbus(QDBusConnection::systemBus()),
+ m_bluezManager("org.bluez", "/", m_dbus),
+ m_bluezAgentManager("org.bluez", "/org/bluez", m_dbus)
+{
+ qDBusRegisterMetaType<InterfaceList>();
+ qDBusRegisterMetaType<ManagedObjectList>();
+
+ if (m_bluezManager.isValid()) {
+ connect(&m_bluezManager, SIGNAL(InterfacesAdded(const QDBusObjectPath&, InterfaceList)),
+ this, SLOT(slotInterfacesAdded(const QDBusObjectPath&, InterfaceList)));
+
+ connect(&m_bluezManager, SIGNAL(InterfacesRemoved(const QDBusObjectPath&, const QStringList&)),
+ this, SLOT(slotInterfacesRemoved(const QDBusObjectPath&, const QStringList&)));
+
+ auto objectList = m_bluezManager.GetManagedObjects().argumentAt<0>();
+ for (QDBusObjectPath path : objectList.keys()) {
+ InterfaceList ifaces = objectList.value(path);
+ if (ifaces.contains(BLUEZ_DEVICE_IFACE)) {
+ QString candidatePath = path.path();
+
+ auto properties = ifaces.value(BLUEZ_DEVICE_IFACE);
+ addDevice(path, properties);
+ }
+ }
+ }
+}
+
+QList<Device> BluezClient::pairedPebbles() const
+{
+ QList<Device> ret;
+ if (m_bluezManager.isValid()) {
+ foreach (const Device &dev, m_devices) {
+ ret << dev;
+ }
+ }
+ return ret;
+}
+
+void BluezClient::addDevice(const QDBusObjectPath &path, const QVariantMap &properties)
+{
+ QString address = properties.value("Address").toString();
+ QString name = properties.value("Name").toString();
+ if (name.startsWith("Pebble") && !name.startsWith("Pebble Time LE") && !name.startsWith("Pebble-LE") && !m_devices.contains(address)) {
+ qDebug() << "Found new Pebble:" << address << name;
+ Device device;
+ device.address = QBluetoothAddress(address);
+ device.name = name;
+ device.path = path.path();
+ m_devices.insert(path.path(), device);
+ qDebug() << "emitting added";
+ emit devicesChanged();
+ }
+}
+
+void BluezClient::slotInterfacesAdded(const QDBusObjectPath &path, InterfaceList ifaces)
+{
+ qDebug() << "Interface added!";
+ if (ifaces.contains(BLUEZ_DEVICE_IFACE)) {
+ auto properties = ifaces.value(BLUEZ_DEVICE_IFACE);
+ addDevice(path, properties);
+ }
+}
+
+void BluezClient::slotInterfacesRemoved(const QDBusObjectPath &path, const QStringList &ifaces)
+{
+ qDebug() << "interfaces removed" << path.path() << ifaces;
+ if (!ifaces.contains(BLUEZ_DEVICE_IFACE)) {
+ return;
+ }
+ if (m_devices.contains(path.path())) {
+ m_devices.take(path.path());
+ qDebug() << "removing dev";
+ emit devicesChanged();
+ }
+}
diff --git a/rockworkd/libpebble/bluez/bluezclient.h b/rockworkd/libpebble/bluez/bluezclient.h
new file mode 100644
index 0000000..f8e5749
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluezclient.h
@@ -0,0 +1,51 @@
+#ifndef BLUEZCLIENT_H
+#define BLUEZCLIENT_H
+
+#include <QList>
+#include <QBluetoothAddress>
+#include <QBluetoothLocalDevice>
+
+#include "bluez_helper.h"
+#include "freedesktop_objectmanager.h"
+#include "freedesktop_properties.h"
+#include "bluez_adapter1.h"
+#include "bluez_agentmanager1.h"
+
+class Device {
+public:
+ QBluetoothAddress address;
+ QString name;
+ QString path;
+};
+
+class BluezClient: public QObject
+{
+ Q_OBJECT
+
+public:
+ BluezClient(QObject *parent = 0);
+
+
+ QList<Device> pairedPebbles() const;
+
+private slots:
+ void addDevice(const QDBusObjectPath &path, const QVariantMap &properties);
+
+ void slotInterfacesAdded(const QDBusObjectPath&path, InterfaceList ifaces);
+ void slotInterfacesRemoved(const QDBusObjectPath&path, const QStringList &ifaces);
+
+signals:
+ void devicesChanged();
+
+private:
+ QDBusConnection m_dbus;
+ DBusObjectManagerInterface m_bluezManager;
+ BluezAgentManager1 m_bluezAgentManager;
+ BluezAdapter1 *m_bluezAdapter = nullptr;
+ FreeDesktopProperties *m_bluezAdapterProperties = nullptr;
+
+
+ QHash<QString, Device> m_devices;
+};
+
+#endif // BLUEZCLIENT_H
diff --git a/rockworkd/libpebble/bluez/dbus-shared.h b/rockworkd/libpebble/bluez/dbus-shared.h
new file mode 100644
index 0000000..01e9699
--- /dev/null
+++ b/rockworkd/libpebble/bluez/dbus-shared.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#ifndef USS_DBUS_SHARED_H
+#define USS_DBUS_SHARED_H
+
+#define DBUS_AGENT_PATH "/com/canonical/SettingsBluetoothAgent"
+#define DBUS_ADAPTER_AGENT_PATH "/com/canonical/SettingsBluetoothAgent/adapteragent"
+#define DBUS_AGENT_CAPABILITY "KeyboardDisplay"
+
+#define BLUEZ_SERVICE "org.bluez"
+
+#define BLUEZ_ADAPTER_IFACE "org.bluez.Adapter1"
+#define BLUEZ_DEVICE_IFACE "org.bluez.Device1"
+
+#define watchCall(call, func) \
+ QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); \
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, func)
+
+#endif // USS_DBUS_SHARED_H
diff --git a/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp b/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp
new file mode 100644
index 0000000..71ca4ce
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -p freedesktop_objectmanager -i bluez_helper.h -v -c DBusObjectManagerInterface org.freedesktop.DBus.ObjectManager.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 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.
+ */
+
+#include "freedesktop_objectmanager.h"
+
+/*
+ * Implementation of interface class DBusObjectManagerInterface
+ */
+
+DBusObjectManagerInterface::DBusObjectManagerInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+DBusObjectManagerInterface::~DBusObjectManagerInterface()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/freedesktop_objectmanager.h b/rockworkd/libpebble/bluez/freedesktop_objectmanager.h
new file mode 100644
index 0000000..509c5fc
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_objectmanager.h
@@ -0,0 +1,58 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -p freedesktop_objectmanager -i bluez_helper.h -v -c DBusObjectManagerInterface org.freedesktop.DBus.ObjectManager.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef FREEDESKTOP_OBJECTMANAGER_H_1442473386
+#define FREEDESKTOP_OBJECTMANAGER_H_1442473386
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+#include "bluez_helper.h"
+
+/*
+ * Proxy class for interface org.freedesktop.DBus.ObjectManager
+ */
+class DBusObjectManagerInterface: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.freedesktop.DBus.ObjectManager"; }
+
+public:
+ DBusObjectManagerInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~DBusObjectManagerInterface();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<ManagedObjectList> GetManagedObjects()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("GetManagedObjects"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+ void InterfacesAdded(const QDBusObjectPath &object_path, InterfaceList interfaces_and_properties);
+ void InterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces);
+};
+
+namespace org {
+ namespace freedesktop {
+ namespace DBus {
+ typedef ::DBusObjectManagerInterface ObjectManager;
+ }
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/freedesktop_properties.cpp b/rockworkd/libpebble/bluez/freedesktop_properties.cpp
new file mode 100644
index 0000000..c74347c
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_properties.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c FreeDesktopProperties -p freedesktop_properties -v org.freedesktop.DBus.Properties.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 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.
+ */
+
+#include "freedesktop_properties.h"
+
+/*
+ * Implementation of interface class FreeDesktopProperties
+ */
+
+FreeDesktopProperties::FreeDesktopProperties(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+FreeDesktopProperties::~FreeDesktopProperties()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/freedesktop_properties.h b/rockworkd/libpebble/bluez/freedesktop_properties.h
new file mode 100644
index 0000000..a7a655c
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_properties.h
@@ -0,0 +1,71 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c FreeDesktopProperties -p freedesktop_properties -v org.freedesktop.DBus.Properties.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef FREEDESKTOP_PROPERTIES_H_1442473392
+#define FREEDESKTOP_PROPERTIES_H_1442473392
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.freedesktop.DBus.Properties
+ */
+class FreeDesktopProperties: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.freedesktop.DBus.Properties"; }
+
+public:
+ FreeDesktopProperties(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~FreeDesktopProperties();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<QDBusVariant> Get(const QString &interface, const QString &name)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(interface) << QVariant::fromValue(name);
+ return asyncCallWithArgumentList(QStringLiteral("Get"), argumentList);
+ }
+
+ inline QDBusPendingReply<QVariantMap> GetAll(const QString &interface)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(interface);
+ return asyncCallWithArgumentList(QStringLiteral("GetAll"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Set(const QString &interface, const QString &name, const QDBusVariant &value)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(interface) << QVariant::fromValue(name) << QVariant::fromValue(value);
+ return asyncCallWithArgumentList(QStringLiteral("Set"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+ void PropertiesChanged(const QString &interface, const QVariantMap &changed_properties, const QStringList &invalidated_properties);
+};
+
+namespace org {
+ namespace freedesktop {
+ namespace DBus {
+ typedef ::FreeDesktopProperties Properties;
+ }
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml b/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml
new file mode 100644
index 0000000..e535c7e
--- /dev/null
+++ b/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml
@@ -0,0 +1,16 @@
+<!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.bluez.AgentManager1">
+ <method name="RegisterAgent">
+ <arg type="o" name="agent"/>
+ <arg type="s" name="capability"/>
+ </method>
+ <method name="UnregisterAgent">
+ <arg type="o" name="agent"/>
+ </method>
+ <method name="RequestDefaultAgent">
+ <arg type="o" name="agent"/>
+ </method>
+ </interface>
+</node>
diff --git a/rockworkd/libpebble/bundle.cpp b/rockworkd/libpebble/bundle.cpp
new file mode 100644
index 0000000..64061c8
--- /dev/null
+++ b/rockworkd/libpebble/bundle.cpp
@@ -0,0 +1,151 @@
+#include "bundle.h"
+
+#include <QVariantMap>
+#include <QFileInfo>
+#include <QDebug>
+#include <QJsonParseError>
+
+Bundle::Bundle(const QString &path):
+ m_path(path)
+{
+
+}
+
+QString Bundle::path() const
+{
+ return m_path;
+}
+
+QString Bundle::file(Bundle::FileType type, HardwarePlatform hardwarePlatform) const
+{
+ // Those two will always be in the top level dir. HardwarePlatform is irrelevant.
+ switch (type) {
+ case FileTypeAppInfo:
+ return m_path + "/appInfo.js";
+ case FileTypeJsApp:
+ return m_path + "/pebble-js-app.js";
+ default:
+ ;
+ }
+
+ // For all the others we have to find the manifest file
+ QList<QString> possibleDirs;
+
+ switch (hardwarePlatform) {
+ case HardwarePlatformAplite:
+ if (QFileInfo::exists(path() + "/aplite/")) {
+ possibleDirs.append("aplite");
+ }
+ possibleDirs.append("");
+ break;
+ case HardwarePlatformBasalt:
+ if (QFileInfo::exists(path() + "/basalt/")) {
+ possibleDirs.append("basalt");
+ }
+ possibleDirs.append("");
+ break;
+ case HardwarePlatformChalk:
+ if (QFileInfo::exists(path() + "/chalk/")) {
+ possibleDirs.append("chalk");
+ }
+ break;
+ default:
+ possibleDirs.append("");
+ ;
+ }
+
+ QString manifestFilename;
+ QString subDir;
+ foreach (const QString &dir, possibleDirs) {
+ if (QFileInfo::exists(m_path + "/" + dir + "/manifest.json")) {
+ subDir = "/" + dir;
+ manifestFilename = m_path + subDir + "/manifest.json";
+ break;
+ }
+ }
+ if (manifestFilename.isEmpty()) {
+ qWarning() << "Error finding manifest.json";
+ return QString();
+ }
+
+ // We want the manifiest file. just return it without parsing it
+ if (type == FileTypeManifest) {
+ return manifestFilename;
+ }
+
+ QFile manifest(manifestFilename);
+ if (!manifest.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening" << manifestFilename;
+ return QString();
+ }
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(manifest.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing" << manifestFilename;
+ return QString();
+ }
+
+ QVariantMap manifestMap = jsonDoc.toVariant().toMap();
+ switch (type) {
+ case FileTypeApplication:
+ return m_path + subDir + "/" + manifestMap.value("application").toMap().value("name").toString();
+ case FileTypeResources:
+ if (manifestMap.contains("resources")) {
+ return m_path + subDir + "/" + manifestMap.value("resources").toMap().value("name").toString();
+ }
+ break;
+ case FileTypeWorker:
+ if (manifestMap.contains("worker")) {
+ return m_path + subDir + "/" + manifestMap.value("worker").toMap().value("name").toString();
+ }
+ break;
+ case FileTypeFirmware:
+ if (manifestMap.contains("firmware")) {
+ return m_path + subDir + "/" + manifestMap.value("firmware").toMap().value("name").toString();
+ }
+ break;
+ default:
+ ;
+ }
+ return QString();
+}
+
+quint32 Bundle::crc(Bundle::FileType type, HardwarePlatform hardwarePlatform) const
+{
+ switch (type) {
+ case FileTypeAppInfo:
+ case FileTypeJsApp:
+ case FileTypeManifest:
+ qWarning() << "Cannot get crc for file type" << type;
+ return 0;
+ default: ;
+ }
+
+ QFile manifest(file(FileTypeManifest, hardwarePlatform));
+ if (!manifest.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening manifest file";
+ return 0;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(manifest.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing manifest file";
+ return 0;
+ }
+
+ QVariantMap manifestMap = jsonDoc.toVariant().toMap();
+ switch (type) {
+ case FileTypeApplication:
+ return manifestMap.value("application").toMap().value("crc").toUInt();
+ case FileTypeResources:
+ return manifestMap.value("resources").toMap().value("crc").toUInt();
+ case FileTypeWorker:
+ return manifestMap.value("worker").toMap().value("crc").toUInt();
+ case FileTypeFirmware:
+ return manifestMap.value("firmware").toMap().value("crc").toUInt();
+ default:
+ ;
+ }
+ return 0;
+}
diff --git a/rockworkd/libpebble/bundle.h b/rockworkd/libpebble/bundle.h
new file mode 100644
index 0000000..5ff5a16
--- /dev/null
+++ b/rockworkd/libpebble/bundle.h
@@ -0,0 +1,33 @@
+#ifndef BUNDLE_H
+#define BUNDLE_H
+
+#include <QString>
+
+#include "enums.h"
+
+class Bundle
+{
+public:
+ enum FileType {
+ FileTypeAppInfo,
+ FileTypeJsApp,
+ FileTypeManifest,
+ FileTypeApplication,
+ FileTypeResources,
+ FileTypeWorker,
+ FileTypeFirmware
+ };
+
+ Bundle(const QString &path = QString());
+
+ QString path() const;
+
+ QString file(FileType type, HardwarePlatform hardwarePlatform = HardwarePlatformUnknown) const;
+ quint32 crc(FileType type, HardwarePlatform hardwarePlatform = HardwarePlatformUnknown) const;
+
+private:
+ QString m_path;
+
+};
+
+#endif // BUNDLE_H
diff --git a/rockworkd/libpebble/calendarevent.cpp b/rockworkd/libpebble/calendarevent.cpp
new file mode 100644
index 0000000..ea99b56
--- /dev/null
+++ b/rockworkd/libpebble/calendarevent.cpp
@@ -0,0 +1,184 @@
+#include "calendarevent.h"
+
+#include <QSettings>
+#include <QFile>
+#include <QTimeZone>
+
+CalendarEvent::CalendarEvent():
+ m_uuid(QUuid::createUuid())
+{
+}
+
+bool CalendarEvent::isValid() const
+{
+ return !m_id.isNull();
+}
+
+QString CalendarEvent::id() const
+{
+ return m_id;
+}
+
+void CalendarEvent::setId(const QString &id)
+{
+ m_id = id;
+}
+
+QUuid CalendarEvent::uuid() const
+{
+ return m_uuid;
+}
+
+void CalendarEvent::generateNewUuid()
+{
+ m_uuid = QUuid::createUuid();
+}
+
+QString CalendarEvent::title() const
+{
+ return m_title;
+}
+
+void CalendarEvent::setTitle(const QString &title)
+{
+ m_title = title;
+}
+
+QString CalendarEvent::description() const
+{
+ return m_description;
+}
+
+void CalendarEvent::setDescription(const QString &description)
+{
+ m_description = description;
+}
+
+QDateTime CalendarEvent::startTime() const
+{
+ return m_startTime;
+}
+
+void CalendarEvent::setStartTime(const QDateTime &startTime)
+{
+ m_startTime = startTime;
+}
+
+QDateTime CalendarEvent::endTime() const
+{
+ return m_endTime;
+}
+
+void CalendarEvent::setEndTime(const QDateTime &endTime)
+{
+ m_endTime = endTime;
+}
+
+QString CalendarEvent::location() const
+{
+ return m_location;
+}
+
+void CalendarEvent::setLocation(const QString &location)
+{
+ m_location = location;
+}
+
+QString CalendarEvent::calendar() const
+{
+ return m_calendar;
+}
+
+void CalendarEvent::setCalendar(const QString &calendar)
+{
+ m_calendar = calendar;
+}
+
+QString CalendarEvent::comment() const
+{
+ return m_comment;
+}
+
+void CalendarEvent::setComment(const QString &comment)
+{
+ m_comment = comment;
+}
+
+QStringList CalendarEvent::guests() const
+{
+ return m_guests;
+}
+
+void CalendarEvent::setGuests(const QStringList &guests)
+{
+ m_guests = guests;
+}
+
+bool CalendarEvent::recurring() const
+{
+ return m_recurring;
+}
+
+void CalendarEvent::setRecurring(bool recurring)
+{
+ m_recurring = recurring;
+}
+
+bool CalendarEvent::operator==(const CalendarEvent &other) const
+{
+ // Storing a QDateTime to QSettings seems to lose time zone information. Lets ignore the time zone when
+ // comparing or we'll never find ourselves again.
+ QDateTime thisStartTime = m_startTime;
+ thisStartTime.setTimeZone(other.startTime().timeZone());
+ QDateTime thisEndTime = m_endTime;
+ thisEndTime.setTimeZone(other.endTime().timeZone());
+ return m_id == other.id()
+ && m_title == other.title()
+ && m_description == other.description()
+ && thisStartTime == other.startTime()
+ && thisEndTime == other.endTime()
+ && m_location == other.location()
+ && m_calendar == other.calendar()
+ && m_comment == other.comment()
+ && m_guests == other.guests()
+ && m_recurring == other.recurring();
+
+}
+
+void CalendarEvent::saveToCache(const QString &cachePath) const
+{
+ QSettings s(cachePath + "/calendarevent-" + m_uuid.toString(), QSettings::IniFormat);
+ s.setValue("id", m_id);
+ s.setValue("uuid", m_uuid);
+ s.setValue("title", m_title);
+ s.setValue("description", m_description);
+ s.setValue("startTime", m_startTime);
+ s.setValue("endTime", m_endTime);
+ s.setValue("location", m_location);
+ s.setValue("calendar", m_calendar);
+ s.setValue("comment", m_comment);
+ s.setValue("guests", m_guests);
+ s.setValue("recurring", m_recurring);
+}
+
+void CalendarEvent::loadFromCache(const QString &cachePath, const QString &uuid)
+{
+ m_uuid = uuid;
+ QSettings s(cachePath + "/calendarevent-" + m_uuid.toString(), QSettings::IniFormat);
+ m_id = s.value("id").toString();
+ m_title = s.value("title").toString();
+ m_description = s.value("description").toString();
+ m_startTime = s.value("startTime").toDateTime();
+ m_endTime = s.value("endTime").toDateTime();
+ m_location = s.value("location").toString();
+ m_calendar = s.value("calendar").toString();
+ m_comment = s.value("comment").toString();
+ m_guests = s.value("guests").toStringList();
+ m_recurring = s.value("recurring").toBool();
+}
+
+void CalendarEvent::removeFromCache(const QString &cachePath) const
+{
+ QFile::remove(cachePath + "/calendarevent-" + m_uuid.toString());
+}
+
diff --git a/rockworkd/libpebble/calendarevent.h b/rockworkd/libpebble/calendarevent.h
new file mode 100644
index 0000000..5361a48
--- /dev/null
+++ b/rockworkd/libpebble/calendarevent.h
@@ -0,0 +1,69 @@
+#ifndef CALENDAREVENT_H
+#define CALENDAREVENT_H
+
+#include <QString>
+#include <QStringList>
+#include <QDateTime>
+#include <QUuid>
+
+class CalendarEvent
+{
+public:
+ CalendarEvent();
+
+ bool isValid() const;
+
+ QString id() const;
+ void setId(const QString &id);
+
+ QUuid uuid() const;
+ void generateNewUuid();
+
+ QString title() const;
+ void setTitle(const QString &title);
+
+ QString description() const;
+ void setDescription(const QString &description);
+
+ QDateTime startTime() const;
+ void setStartTime(const QDateTime &startTime);
+
+ QDateTime endTime() const;
+ void setEndTime(const QDateTime &endTime);
+
+ QString location() const;
+ void setLocation(const QString &location);
+
+ QString calendar() const;
+ void setCalendar(const QString &calendar);
+
+ QString comment() const;
+ void setComment(const QString &comment);
+
+ QStringList guests() const;
+ void setGuests(const QStringList &guests);
+
+ bool recurring() const;
+ void setRecurring(bool recurring);
+
+ bool operator==(const CalendarEvent &other) const;
+
+ void saveToCache(const QString &cachePath) const;
+ void loadFromCache(const QString &cachePath, const QString &uuid);
+ void removeFromCache(const QString &cachePath) const;
+
+private:
+ QString m_id;
+ QUuid m_uuid;
+ QString m_title;
+ QString m_description;
+ QDateTime m_startTime;
+ QDateTime m_endTime;
+ QString m_location;
+ QString m_calendar;
+ QString m_comment;
+ QStringList m_guests;
+ bool m_recurring = false;
+};
+
+#endif // CALENDAREVENT_H
diff --git a/rockworkd/libpebble/dataloggingendpoint.cpp b/rockworkd/libpebble/dataloggingendpoint.cpp
new file mode 100644
index 0000000..a571c25
--- /dev/null
+++ b/rockworkd/libpebble/dataloggingendpoint.cpp
@@ -0,0 +1,44 @@
+#include "dataloggingendpoint.h"
+
+#include "pebble.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+DataLoggingEndpoint::DataLoggingEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointDataLogging, this, "handleMessage");
+}
+
+void DataLoggingEndpoint::handleMessage(const QByteArray &data)
+{
+ qDebug() << "data logged" << data.toHex();
+ WatchDataReader reader(data);
+ DataLoggingCommand command = (DataLoggingCommand)reader.read<quint8>();
+ switch (command) {
+ case DataLoggingDespoolSendData: {
+ quint8 sessionId = reader.read<quint8>();
+ quint32 itemsLeft = reader.readLE<quint32>();
+ quint32 crc = reader.readLE<quint32>();
+ qDebug() << "Despooling data: Session:" << sessionId << "Items left:" << itemsLeft << "CRC:" << crc;
+
+ QByteArray reply;
+ WatchDataWriter writer(&reply);
+ writer.write<quint8>(DataLoggingACK);
+ writer.write<quint8>(sessionId);
+ m_connection->writeToPebble(WatchConnection::EndpointDataLogging, reply);
+ return;
+ }
+ case DataLoggingTimeout: {
+ quint8 sessionId = reader.read<quint8>();
+ qDebug() << "DataLogging reached timeout: Session:" << sessionId;
+ return;
+ }
+ default:
+ qDebug() << "Unhandled DataLogging message";
+ }
+}
+
diff --git a/rockworkd/libpebble/dataloggingendpoint.h b/rockworkd/libpebble/dataloggingendpoint.h
new file mode 100644
index 0000000..2c5dfc5
--- /dev/null
+++ b/rockworkd/libpebble/dataloggingendpoint.h
@@ -0,0 +1,39 @@
+#ifndef DATALOGGINGENDPOINT_H
+#define DATALOGGINGENDPOINT_H
+
+#include <QObject>
+
+class Pebble;
+class WatchConnection;
+
+class DataLoggingEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum DataLoggingCommand {
+ DataLoggingDespoolOpenSession = 0x01,
+ DataLoggingDespoolSendData = 0x02,
+ DataLoggingCloseSession = 0x03,
+ DataLoggingReportOpenSessions = 0x84,
+ DataLoggingACK = 0x85,
+ DataLoggingNACK = 0x86,
+ DataLoggingTimeout = 0x07,
+ DataLoggingEmptySession = 0x88,
+ DataLoggingGetSendEnableRequest = 0x89,
+ DataLoggingGetSendEnableResponse = 0x0A,
+ DataLoggingSetSendEnable = 0x8B
+ };
+
+ explicit DataLoggingEndpoint(Pebble *pebble, WatchConnection *connection);
+
+signals:
+
+private slots:
+ void handleMessage(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+};
+
+#endif // DATALOGGINGENDPOINT_H
diff --git a/rockworkd/libpebble/enums.h b/rockworkd/libpebble/enums.h
new file mode 100644
index 0000000..d6184c6
--- /dev/null
+++ b/rockworkd/libpebble/enums.h
@@ -0,0 +1,95 @@
+#ifndef ENUMS_H
+#define ENUMS_H
+
+#include <QMetaType>
+
+enum HardwareRevision {
+ HardwareRevisionUNKNOWN = 0,
+ HardwareRevisionTINTIN_EV1 = 1,
+ HardwareRevisionTINTIN_EV2 = 2,
+ HardwareRevisionTINTIN_EV2_3 = 3,
+ HardwareRevisionTINTIN_EV2_4 = 4,
+ HardwareRevisionTINTIN_V1_5 = 5,
+ HardwareRevisionBIANCA = 6,
+ HardwareRevisionSNOWY_EVT2 = 7,
+ HardwareRevisionSNOWY_DVT = 8,
+ HardwareRevisionSPALDING_EVT = 9,
+ HardwareRevisionBOBBY_SMILES = 10,
+ HardwareRevisionSPALDING = 11,
+
+ HardwareRevisionTINTIN_BB = 0xFF,
+ HardwareRevisionTINTIN_BB2 = 0xFE,
+ HardwareRevisionSNOWY_BB = 0xFD,
+ HardwareRevisionSNOWY_BB2 = 0xFC,
+ HardwareRevisionSPALDING_BB2 = 0xFB
+};
+
+enum OS {
+ OSUnknown = 0,
+ OSiOS = 1,
+ OSAndroid = 2,
+ OSOSX = 3,
+ OSLinux = 4,
+ OSWindows = 5
+};
+
+enum HardwarePlatform {
+ HardwarePlatformUnknown = 0,
+ HardwarePlatformAplite,
+ HardwarePlatformBasalt,
+ HardwarePlatformChalk
+};
+
+enum Model {
+ ModelUnknown = 0,
+ ModelTintinBlack = 1,
+ ModelTintinWhite = 2,
+ ModelTintinRed = 3,
+ ModelTintinOrange = 4,
+ ModelTintinGrey = 5,
+ ModelBiancaSilver = 6,
+ ModelBiancaBlack = 7,
+ ModelTintinBlue = 8,
+ ModelTintinGreen = 9,
+ ModelTintinPink = 10,
+ ModelSnowyWhite = 11,
+ ModelSnowyBlack = 12,
+ ModelSnowyRed = 13,
+ ModelBobbySilver = 14,
+ ModelBobbyBlack = 15,
+ ModelBobbyGold = 16,
+ ModelSpalding14Silver = 17,
+ ModelSpalding14Black = 18,
+ ModelSpalding20Silver = 19,
+ ModelSpalding20Black = 20,
+ ModelSpalding14RoseGold = 21
+};
+
+enum MusicControlButton {
+ MusicControlPlayPause,
+ MusicControlSkipBack,
+ MusicControlSkipNext,
+ MusicControlVolumeUp,
+ MusicControlVolumeDown
+};
+
+enum CallStatus {
+ CallStatusIncoming,
+ CallStatusOutGoing
+};
+
+enum Capability {
+ CapabilityNone = 0x0000000000000000,
+ CapabilityAppRunState = 0x0000000000000001,
+ CapabilityInfiniteLogDumping = 0x0000000000000002,
+ CapabilityUpdatedMusicProtocol = 0x0000000000000004,
+ CapabilityExtendedNotifications = 0x0000000000000008,
+ CapabilityLanguagePacks = 0x0000000000000010,
+ Capability8kAppMessages = 0x0000000000000020,
+ CapabilityHealth = 0x0000000000000040,
+ CapabilityVoice = 0x0000000000000080
+};
+Q_DECLARE_FLAGS(Capabilities, Capability)
+
+#endif // ENUMS_H
+
diff --git a/rockworkd/libpebble/firmwaredownloader.cpp b/rockworkd/libpebble/firmwaredownloader.cpp
new file mode 100644
index 0000000..5d32f3b
--- /dev/null
+++ b/rockworkd/libpebble/firmwaredownloader.cpp
@@ -0,0 +1,246 @@
+#include "firmwaredownloader.h"
+#include "ziphelper.h"
+#include "pebble.h"
+#include "watchconnection.h"
+#include "uploadmanager.h"
+
+#include <QNetworkAccessManager>
+#include <QUrlQuery>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QJsonDocument>
+#include <QFile>
+#include <QDir>
+#include <QCryptographicHash>
+
+FirmwareDownloader::FirmwareDownloader(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_nam = new QNetworkAccessManager(this);
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointSystemMessage, this, "systemMessageReceived");
+}
+
+bool FirmwareDownloader::updateAvailable() const
+{
+ return m_updateAvailable;
+}
+
+QString FirmwareDownloader::candidateVersion() const
+{
+ return m_candidateVersion;
+}
+
+QString FirmwareDownloader::releaseNotes() const
+{
+ return m_releaseNotes;
+}
+
+QString FirmwareDownloader::url() const
+{
+ return m_url;
+}
+
+bool FirmwareDownloader::upgrading() const
+{
+ return m_upgradeInProgress;
+}
+
+void FirmwareDownloader::performUpgrade()
+{
+ if (!m_updateAvailable) {
+ qWarning() << "No update available";
+ return;
+ }
+
+ if (m_upgradeInProgress) {
+ qWarning() << "Upgrade already in progress. Won't start another one";
+ return;
+ }
+
+ m_upgradeInProgress = true;
+ emit upgradingChanged();
+
+ QNetworkRequest request(m_url);
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply](){
+ reply->deleteLater();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qWarning() << "Erorr fetching firmware" << reply->errorString();
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+
+ QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex();
+
+ if (hash != m_hash) {
+ qWarning() << "Downloaded data hash doesn't match hash from target";
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ QDir dir("/tmp/" + m_pebble->address().toString().replace(":", "_"));
+ if (!dir.exists() && !dir.mkpath(dir.absolutePath())) {
+ qWarning() << "Error saving file" << dir.absolutePath();
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+ QString path = "/tmp/" + m_pebble->address().toString().replace(":", "_");
+ QFile f(path + "/" + reply->request().url().fileName());
+ if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Cannot open tmp file for writing" << f.fileName();
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+ f.write(data);
+ f.close();
+
+ if (!ZipHelper::unpackArchive(f.fileName(), path)) {
+ qWarning() << "Error unpacking firmware archive";
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ Bundle firmware(path);
+ if (firmware.file(Bundle::FileTypeFirmware).isEmpty() || firmware.file(Bundle::FileTypeResources).isEmpty()) {
+ qWarning() << "Firmware bundle file missing binary or resources";
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ qDebug() << "** Starting firmware upgrade **";
+ m_bundlePath = path;
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareStart);
+
+ });
+}
+
+void FirmwareDownloader::checkForNewFirmware()
+{
+ QString platformString;
+ switch (m_pebble->hardwareRevision()) {
+ case HardwareRevisionUNKNOWN:
+ case HardwareRevisionTINTIN_EV1:
+ case HardwareRevisionTINTIN_EV2:
+ case HardwareRevisionTINTIN_EV2_3:
+ case HardwareRevisionSNOWY_EVT2:
+ case HardwareRevisionSPALDING_EVT:
+ case HardwareRevisionTINTIN_BB:
+ case HardwareRevisionTINTIN_BB2:
+ case HardwareRevisionSNOWY_BB:
+ case HardwareRevisionSNOWY_BB2:
+ case HardwareRevisionSPALDING_BB2:
+ qWarning() << "Hardware revision not supported for firmware upgrades" << m_pebble->hardwareRevision();
+ return;
+ case HardwareRevisionTINTIN_EV2_4:
+ platformString = "ev2_4";
+ break;
+ case HardwareRevisionTINTIN_V1_5:
+ platformString = "v1_5";
+ break;
+ case HardwareRevisionBIANCA:
+ platformString = "v2_0";
+ break;
+ case HardwareRevisionSNOWY_DVT:
+ platformString = "snowy_dvt";
+ break;
+ case HardwareRevisionBOBBY_SMILES:
+ platformString = "snowy_s3";
+ break;
+ case HardwareRevisionSPALDING:
+ platformString = "spalding";
+ break;
+
+ }
+
+ QString url("https://pebblefw.s3.amazonaws.com/pebble/%1/%2/latest.json");
+ url = url.arg(platformString).arg("release-v3.8");
+ QNetworkRequest request(url);
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing firmware fetch reply" << jsonDoc.toJson(QJsonDocument::Indented);
+ return;
+ }
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+ if (!resultMap.contains("normal")) {
+ qWarning() << "Could not find normal firmware package" << jsonDoc.toJson(QJsonDocument::Indented);
+ return;
+ }
+
+
+ QVariantMap targetFirmware;
+ if (resultMap.contains("3.x-migration") && m_pebble->softwareVersion() < "v3.0.0") {
+ targetFirmware = resultMap.value("3.x-migration").toMap();
+ } else if (m_pebble->softwareVersion() >= "v3.0.0" &&
+ resultMap.value("normal").toMap().value("friendlyVersion").toString() > m_pebble->softwareVersion()){
+ targetFirmware = resultMap.value("normal").toMap();
+ }
+
+ if (targetFirmware.isEmpty()) {
+ qDebug() << "Watch firmware is up to date";
+ m_updateAvailable = false;
+ emit updateAvailableChanged();
+ return;
+ }
+
+ qDebug() << targetFirmware;
+
+ m_candidateVersion = targetFirmware.value("friendlyVersion").toString();
+ m_releaseNotes = targetFirmware.value("notes").toString();
+ m_url = targetFirmware.value("url").toString();
+ m_hash = targetFirmware.value("sha-256").toByteArray();
+ m_updateAvailable = true;
+ qDebug() << "candidate firmware upgrade" << m_candidateVersion << m_releaseNotes << m_url;
+ emit updateAvailableChanged();
+ });
+}
+
+void FirmwareDownloader::systemMessageReceived(const QByteArray &data)
+{
+ qDebug() << "system message" << data.toHex();
+
+ if (!m_upgradeInProgress) {
+ return;
+ }
+
+ Bundle firmware(m_bundlePath);
+
+ qDebug() << "** Uploading firmware resources...";
+ m_connection->uploadManager()->uploadFirmwareResources(firmware.file(Bundle::FileTypeResources), firmware.crc(Bundle::FileTypeResources), [this, firmware]() {
+ qDebug() << "** Firmware resources uploaded. OK";
+
+ qDebug() << "** Uploading firmware binary...";
+ m_connection->uploadManager()->uploadFirmwareBinary(false, firmware.file(Bundle::FileTypeFirmware), firmware.crc(Bundle::FileTypeFirmware), [this]() {
+ qDebug() << "** Firmware binary uploaded. OK";
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareComplete);
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ }, [this](int code) {
+ qWarning() << "** ERROR uploading firmware binary" << code;
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareFail);
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ });
+ },
+ [this](int code) {
+ qWarning() << "** ERROR uploading firmware resources" << code;
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareFail);
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ });
+}
+
diff --git a/rockworkd/libpebble/firmwaredownloader.h b/rockworkd/libpebble/firmwaredownloader.h
new file mode 100644
index 0000000..d7bd5b8
--- /dev/null
+++ b/rockworkd/libpebble/firmwaredownloader.h
@@ -0,0 +1,50 @@
+#ifndef FIRWAREDOWNLOADER_H
+#define FIRWAREDOWNLOADER_H
+
+#include <QObject>
+
+#include "watchconnection.h"
+
+class Pebble;
+class QNetworkAccessManager;
+
+class FirmwareDownloader : public QObject
+{
+ Q_OBJECT
+public:
+ explicit FirmwareDownloader(Pebble *pebble, WatchConnection *connection);
+
+ bool updateAvailable() const;
+ QString candidateVersion() const;
+ QString releaseNotes() const;
+ QString url() const;
+
+ bool upgrading() const;
+
+public slots:
+ void checkForNewFirmware();
+ void performUpgrade();
+
+signals:
+ void updateAvailableChanged();
+ void upgradingChanged();
+
+private slots:
+ void systemMessageReceived(const QByteArray &data);
+
+private:
+ QNetworkAccessManager *m_nam;
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+
+ bool m_updateAvailable = false;
+ QString m_candidateVersion;
+ QString m_releaseNotes;
+ QString m_url;
+ QByteArray m_hash;
+
+ bool m_upgradeInProgress = false;
+ QString m_bundlePath;
+};
+
+#endif // FIRWAREDOWNLOADER_H
diff --git a/rockworkd/libpebble/healthparams.cpp b/rockworkd/libpebble/healthparams.cpp
new file mode 100644
index 0000000..270d950
--- /dev/null
+++ b/rockworkd/libpebble/healthparams.cpp
@@ -0,0 +1,93 @@
+#include "healthparams.h"
+
+#include "watchdatawriter.h"
+
+HealthParams::HealthParams()
+{
+
+}
+
+bool HealthParams::enabled() const
+{
+ return m_enabled;
+}
+
+void HealthParams::setEnabled(bool enabled)
+{
+ m_enabled = enabled;
+}
+
+int HealthParams::height() const
+{
+ return m_height;
+}
+
+void HealthParams::setHeight(int height)
+{
+ m_height = height;
+}
+
+int HealthParams::weight() const
+{
+ return m_weight;
+}
+
+void HealthParams::setWeight(int weight)
+{
+ m_weight = weight;
+}
+
+bool HealthParams::moreActive() const
+{
+ return m_moreActive;
+}
+
+void HealthParams::setMoreActive(bool moreActive)
+{
+ m_moreActive = moreActive;
+}
+
+bool HealthParams::sleepMore() const
+{
+ return m_sleepMore;
+}
+
+void HealthParams::setSleepMore(bool sleepMore)
+{
+ m_sleepMore = sleepMore;
+}
+
+int HealthParams::age() const
+{
+ return m_age;
+}
+
+void HealthParams::setAge(int age)
+{
+ m_age = age;
+}
+
+HealthParams::Gender HealthParams::gender() const
+{
+ return m_gender;
+}
+
+void HealthParams::setGender(HealthParams::Gender gender)
+{
+ m_gender = gender;
+}
+
+QByteArray HealthParams::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.writeLE<quint16>(m_height * 10);
+ writer.writeLE<quint16>(m_weight * 100);
+ writer.write<quint8>(m_enabled ? 0x01 : 0x00);
+ writer.write<quint8>(m_moreActive ? 0x01 : 0x00);
+ writer.write<quint8>(m_sleepMore ? 0x01 : 0x00);
+ writer.write<quint8>(m_age);
+ writer.write<quint8>(m_gender);
+ return ret;
+}
+
diff --git a/rockworkd/libpebble/healthparams.h b/rockworkd/libpebble/healthparams.h
new file mode 100644
index 0000000..03dbfc1
--- /dev/null
+++ b/rockworkd/libpebble/healthparams.h
@@ -0,0 +1,52 @@
+#ifndef HEALTHPARAMS_H
+#define HEALTHPARAMS_H
+
+#include "watchconnection.h"
+
+class HealthParams: public PebblePacket
+{
+public:
+ enum Gender {
+ GenderFemale = 0x00,
+ GenderMale = 0x01
+ };
+
+ HealthParams();
+
+ bool enabled() const;
+ void setEnabled(bool enabled);
+
+ // In cm
+ int height() const;
+ void setHeight(int height);
+
+ // In kg
+ int weight() const;
+ void setWeight(int weight);
+
+ bool moreActive() const;
+ void setMoreActive(bool moreActive);
+
+ bool sleepMore() const;
+ void setSleepMore(bool sleepMore);
+
+ int age() const;
+ void setAge(int age);
+
+ Gender gender() const;
+ void setGender(Gender gender);
+
+ QByteArray serialize() const;
+
+private:
+ bool m_enabled = false;
+ int m_height = 0;
+ int m_weight = 0;
+ bool m_moreActive = false;
+ bool m_sleepMore = false;
+ int m_age = 0;
+ Gender m_gender = Gender::GenderFemale;
+
+};
+
+#endif // HEALTHPARAMS_H
diff --git a/rockworkd/libpebble/jskit/cacheLocalStorage.js b/rockworkd/libpebble/jskit/cacheLocalStorage.js
new file mode 100644
index 0000000..22588a9
--- /dev/null
+++ b/rockworkd/libpebble/jskit/cacheLocalStorage.js
@@ -0,0 +1,11 @@
+//Since we don't have JS 6 support, this hack will allow us to save changes to localStorage when using dot or square bracket notation
+
+for (var key in localStorage) {
+ _jskit.localstorage.setItem(key, localStorage.getItem(key));
+}
+
+for (var key in _jskit.localstorage.keys()) {
+ if (localStorage[key] === undefined) {
+ _jskit.localstorage.removeItem(key);
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jsfiles.qrc b/rockworkd/libpebble/jskit/jsfiles.qrc
new file mode 100644
index 0000000..4dfbc1d
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jsfiles.qrc
@@ -0,0 +1,7 @@
+<RCC>
+ <qresource prefix="/">
+ <file>typedarray.js</file>
+ <file>jskitsetup.js</file>
+ <file>cacheLocalStorage.js</file>
+ </qresource>
+</RCC>
diff --git a/rockworkd/libpebble/jskit/jskitconsole.cpp b/rockworkd/libpebble/jskit/jskitconsole.cpp
new file mode 100644
index 0000000..3d6c85c
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitconsole.cpp
@@ -0,0 +1,29 @@
+#include <QDebug>
+
+#include "jskitconsole.h"
+
+JSKitConsole::JSKitConsole(QObject *parent) :
+ QObject(parent),
+ l(metaObject()->className())
+{
+}
+
+void JSKitConsole::log(const QString &msg)
+{
+ qCDebug(l) << msg;
+}
+
+void JSKitConsole::warn(const QString &msg)
+{
+ qCWarning(l) << msg;
+}
+
+void JSKitConsole::error(const QString &msg)
+{
+ qCCritical(l) << msg;
+}
+
+void JSKitConsole::info(const QString &msg)
+{
+ qCDebug(l) << msg;
+}
diff --git a/rockworkd/libpebble/jskit/jskitconsole.h b/rockworkd/libpebble/jskit/jskitconsole.h
new file mode 100644
index 0000000..3896ae3
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitconsole.h
@@ -0,0 +1,20 @@
+#ifndef JSKITCONSOLE_H
+#define JSKITCONSOLE_H
+
+#include <QLoggingCategory>
+
+class JSKitConsole : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitConsole(QObject *parent=0);
+
+ Q_INVOKABLE void log(const QString &msg);
+ Q_INVOKABLE void warn(const QString &msg);
+ Q_INVOKABLE void error(const QString &msg);
+ Q_INVOKABLE void info(const QString &msg);
+};
+
+#endif // JSKITCONSOLE_H
diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.cpp b/rockworkd/libpebble/jskit/jskitgeolocation.cpp
new file mode 100644
index 0000000..409cda1
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitgeolocation.cpp
@@ -0,0 +1,302 @@
+#include <limits>
+
+#include "jskitgeolocation.h"
+
+JSKitGeolocation::JSKitGeolocation(QJSEngine *engine) :
+ QObject(engine),
+ l(metaObject()->className()),
+ m_engine(engine),
+ m_source(0),
+ m_lastWatcherId(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 watcherId)
+{
+ removeWatcher(watcherId);
+}
+
+void JSKitGeolocation::handleError(QGeoPositionInfoSource::Error error)
+{
+ qCWarning(l) << "positioning error: " << error;
+
+ if (m_watchers.empty()) {
+ qCWarning(l) << "got position error but no one is watching";
+ stopAndRemove();
+ }
+ else {
+ QJSValue obj;
+ if (error == QGeoPositionInfoSource::AccessError) {
+ obj = buildPositionErrorObject(PERMISSION_DENIED, "permission denied");
+ } else {
+ obj = buildPositionErrorObject(POSITION_UNAVAILABLE, "position unavailable");
+ }
+
+ for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) {
+ invokeCallback(it->errorCallback, obj);
+
+ if (it->once) {
+ it = m_watchers.erase(it);
+ } else {
+ it->timer.restart();
+ ++it;
+ }
+ }
+ }
+}
+
+void JSKitGeolocation::handlePosition(const QGeoPositionInfo &pos)
+{
+ qCDebug(l) << "got position at" << pos.timestamp() << "type" << pos.coordinate().type();
+
+ if (m_watchers.empty()) {
+ qCWarning(l) << "got position update but no one is watching";
+ stopAndRemove();
+ }
+ else {
+ QJSValue obj = buildPositionObject(pos);
+
+ for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) {
+ invokeCallback(it->successCallback, obj);
+
+ if (it->once) {
+ it = m_watchers.erase(it);
+ } else {
+ it->timer.restart();
+ ++it;
+ }
+ }
+ }
+}
+
+void JSKitGeolocation::handleTimeout()
+{
+ qCDebug(l) << "positioning timeout";
+
+ if (m_watchers.empty()) {
+ qCWarning(l) << "got position timeout but no one is watching";
+ stopAndRemove();
+ }
+ else {
+ QJSValue obj = buildPositionErrorObject(TIMEOUT, "timeout");
+
+ for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) {
+ if (it->timer.hasExpired(it->timeout)) {
+ qCDebug(l) << "positioning timeout for watch" << it->watcherId
+ << ", watch is" << it->timer.elapsed() << "ms old, timeout is" << it->timeout;
+ invokeCallback(it->errorCallback, obj);
+
+ if (it->once) {
+ it = m_watchers.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;
+
+ Q_FOREACH(const Watcher &watcher, m_watchers) {
+ qint64 rem_timeout = watcher.timeout - watcher.timer.elapsed();
+ qCDebug(l) << "watch" << watcher.watcherId << "rem timeout" << rem_timeout;
+
+ if (rem_timeout >= 0) {
+ // Make sure the limits aren't 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;
+ m_source->setUpdateInterval(updates_timeout);
+ m_source->startUpdates();
+ } else {
+ qCDebug(l) << "stopping updates";
+ m_source->stopUpdates();
+ }
+
+ if (once_timeout >= 0) {
+ qCDebug(l) << "requesting single location update with timeout" << once_timeout;
+ m_source->requestUpdate(once_timeout);
+ }
+
+ if (once_timeout == 0 && updates_timeout == 0) {
+ stopAndRemove();
+ }
+}
+
+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", false).toBool();
+ watcher.timeout = options.value("timeout", std::numeric_limits<int>::max() - 1).toInt();
+ watcher.maximumAge = options.value("maximumAge", 0).toLongLong();
+ watcher.once = once;
+ watcher.watcherId = ++m_lastWatcherId;
+
+ qCDebug(l) << "setting up watcher, gps=" << watcher.highAccuracy << "timeout=" << watcher.timeout << "maximumAge=" << watcher.maximumAge << "once=" << watcher.once;
+
+ if (!m_source) {
+ m_source = QGeoPositionInfoSource::createDefaultSource(this);
+
+ connect(m_source, static_cast<void (QGeoPositionInfoSource::*)(QGeoPositionInfoSource::Error)>(&QGeoPositionInfoSource::error),
+ this, &JSKitGeolocation::handleError);
+ connect(m_source, &QGeoPositionInfoSource::positionUpdated,
+ this, &JSKitGeolocation::handlePosition);
+ connect(m_source, &QGeoPositionInfoSource::updateTimeout,
+ this, &JSKitGeolocation::handleTimeout);
+ }
+
+ if (watcher.maximumAge > 0) {
+ QDateTime threshold = QDateTime::currentDateTime().addMSecs(-qint64(watcher.maximumAge));
+ QGeoPositionInfo pos = m_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();
+ m_watchers.append(watcher);
+
+ qCDebug(l) << "added new watcher" << watcher.watcherId;
+ QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection);
+
+ return watcher.watcherId;
+}
+
+void JSKitGeolocation::removeWatcher(int watcherId)
+{
+ Watcher watcher;
+
+ qCDebug(l) << "removing watcherId" << watcher.watcherId;
+
+ for (int i = 0; i < m_watchers.size(); i++) {
+ if (m_watchers[i].watcherId == watcherId) {
+ watcher = m_watchers.takeAt(i);
+ break;
+ }
+ }
+
+ if (watcher.watcherId != watcherId) {
+ qCWarning(l) << "watcherId not found";
+ return;
+ }
+
+ QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection);
+}
+
+QJSValue JSKitGeolocation::buildPositionObject(const QGeoPositionInfo &pos)
+{
+ QJSValue obj = m_engine->newObject();
+ QJSValue coords = m_engine->newObject();
+ QJSValue timestamp = m_engine->toScriptValue<quint64>(pos.timestamp().toMSecsSinceEpoch());
+
+ coords.setProperty("latitude", m_engine->toScriptValue(pos.coordinate().latitude()));
+ coords.setProperty("longitude", m_engine->toScriptValue(pos.coordinate().longitude()));
+ if (pos.coordinate().type() == QGeoCoordinate::Coordinate3D) {
+ coords.setProperty("altitude", m_engine->toScriptValue(pos.coordinate().altitude()));
+ } else {
+ coords.setProperty("altitude", m_engine->toScriptValue<void*>(0));
+ }
+
+ coords.setProperty("accuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::HorizontalAccuracy)));
+
+ if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ coords.setProperty("altitudeAccuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::VerticalAccuracy)));
+ } else {
+ coords.setProperty("altitudeAccuracy", m_engine->toScriptValue<void*>(0));
+ }
+
+ if (pos.hasAttribute(QGeoPositionInfo::Direction)) {
+ coords.setProperty("heading", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::Direction)));
+ } else {
+ coords.setProperty("heading", m_engine->toScriptValue<void*>(0));
+ }
+
+ if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) {
+ coords.setProperty("speed", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::GroundSpeed)));
+ } else {
+ coords.setProperty("speed", m_engine->toScriptValue<void*>(0));
+ }
+
+ obj.setProperty("coords", coords);
+ obj.setProperty("timestamp", timestamp);
+
+ return obj;
+}
+
+QJSValue JSKitGeolocation::buildPositionErrorObject(PositionError error, const QString &message)
+{
+ QJSValue obj = m_engine->newObject();
+
+ obj.setProperty("code", m_engine->toScriptValue<unsigned short>(error));
+ obj.setProperty("message", m_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) << "error while invoking callback: " << QString("%1:%2: %3")
+ .arg(result.property("fileName").toString())
+ .arg(result.property("lineNumber").toInt())
+ .arg(result.toString());
+ }
+ } else {
+ qCWarning(l) << "callback is not callable";
+ }
+}
+
+void JSKitGeolocation::stopAndRemove()
+{
+ if (m_source) {
+ qCDebug(l) << "removing source";
+
+ m_source->stopUpdates();
+ m_source->deleteLater();
+ m_source = 0;
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.h b/rockworkd/libpebble/jskit/jskitgeolocation.h
new file mode 100644
index 0000000..582ab32
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitgeolocation.h
@@ -0,0 +1,66 @@
+#ifndef JSKITGEOLOCATION_H
+#define JSKITGEOLOCATION_H
+
+#include <QElapsedTimer>
+#include <QGeoPositionInfoSource>
+#include <QJSValue>
+#include <QLoggingCategory>
+#include <QJSEngine>
+
+class JSKitGeolocation : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+ struct Watcher;
+
+public:
+ explicit JSKitGeolocation(QJSEngine *engine);
+
+ enum PositionError {
+ PERMISSION_DENIED = 1,
+ POSITION_UNAVAILABLE = 2,
+ TIMEOUT = 3
+ };
+ Q_ENUMS(PositionError);
+
+ 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 watcherId);
+
+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 watcherId);
+
+ 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);
+ void stopAndRemove();
+
+private:
+ QJSEngine *m_engine;
+ QGeoPositionInfoSource *m_source;
+
+ struct Watcher {
+ QJSValue successCallback;
+ QJSValue errorCallback;
+ int watcherId;
+ bool once;
+ bool highAccuracy;
+ int timeout;
+ QElapsedTimer timer;
+ qlonglong maximumAge;
+ };
+
+ QList<Watcher> m_watchers;
+ int m_lastWatcherId;
+};
+
+#endif // JSKITGEOLOCATION_H
diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.cpp b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp
new file mode 100644
index 0000000..d69b6ad
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp
@@ -0,0 +1,117 @@
+#include <QDesktopServices>
+#include <QDir>
+#include <QDebug>
+
+#include "jskitlocalstorage.h"
+
+JSKitLocalStorage::JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid):
+ QObject(engine),
+ m_engine(engine),
+ m_storage(new QSettings(getStorageFileFor(storagePath, uuid), QSettings::IniFormat, this))
+{
+}
+
+int JSKitLocalStorage::length() const
+{
+ return m_storage->allKeys().size();
+}
+
+QJSValue JSKitLocalStorage::getItem(const QJSValue &key) const
+{
+ QVariant value = m_storage->value(key.toString());
+
+ if (value.isValid()) {
+ return QJSValue(value.toString());
+ } else {
+ return QJSValue(QJSValue::NullValue);
+ }
+}
+
+bool JSKitLocalStorage::setItem(const QJSValue &key, const QJSValue &value)
+{
+ m_storage->setValue(key.toString(), QVariant::fromValue(value.toString()));
+ return true;
+}
+
+bool JSKitLocalStorage::removeItem(const QJSValue &key)
+{
+ if (m_storage->contains(key.toString())) {
+ m_storage->remove(key.toString());
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void JSKitLocalStorage::clear()
+{
+ m_storage->clear();
+}
+
+QJSValue JSKitLocalStorage::key(int index)
+{
+ QStringList allKeys = m_storage->allKeys();
+ QJSValue key(QJSValue::NullValue);
+
+ if (allKeys.size() > index) {
+ key = QJSValue(allKeys[index]);
+ }
+
+ return key;
+}
+
+QJSValue JSKitLocalStorage::get(const QJSValue &proxy, const QJSValue &key) const
+{
+ Q_UNUSED(proxy);
+ return getItem(key);
+}
+
+bool JSKitLocalStorage::set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value)
+{
+ Q_UNUSED(proxy);
+ return setItem(key, value);
+}
+
+bool JSKitLocalStorage::has(const QJSValue &proxy, const QJSValue &key)
+{
+ Q_UNUSED(proxy);
+ return m_storage->contains(key.toString());
+}
+
+bool JSKitLocalStorage::deleteProperty(const QJSValue &proxy, const QJSValue &key)
+{
+ Q_UNUSED(proxy);
+ return removeItem(key);
+}
+
+QJSValue JSKitLocalStorage::keys(const QJSValue &proxy)
+{
+ Q_UNUSED(proxy);
+
+ QStringList allKeys = m_storage->allKeys();
+ QJSValue keyArray = m_engine->newArray(allKeys.size());
+ for (int i = 0; i < allKeys.size(); i++) {
+ keyArray.setProperty(i, allKeys[i]);
+ }
+
+ return keyArray;
+}
+
+QJSValue JSKitLocalStorage::enumerate()
+{
+ return keys(0);
+}
+
+QString JSKitLocalStorage::getStorageFileFor(const QString &storageDir, const QUuid &uuid)
+{
+ QDir dataDir(storageDir + "/js-storage");
+ if (!dataDir.exists() && !dataDir.mkpath(dataDir.absolutePath())) {
+ qWarning() << "Error creating jskit storage dir";
+ return QString();
+ }
+
+ QString fileName = uuid.toString();
+ fileName.remove('{');
+ fileName.remove('}');
+ return dataDir.absoluteFilePath(fileName + ".ini");
+}
diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.h b/rockworkd/libpebble/jskit/jskitlocalstorage.h
new file mode 100644
index 0000000..9719f83
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitlocalstorage.h
@@ -0,0 +1,40 @@
+#ifndef JSKITLOCALSTORAGE_P_H
+#define JSKITLOCALSTORAGE_P_H
+
+#include <QSettings>
+#include <QJSEngine>
+#include <QUuid>
+
+class JSKitLocalStorage : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int length READ length)
+
+public:
+ explicit JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid);
+
+ int length() const;
+
+ Q_INVOKABLE QJSValue getItem(const QJSValue &key) const;
+ Q_INVOKABLE bool setItem(const QJSValue &key, const QJSValue &value);
+ Q_INVOKABLE bool removeItem(const QJSValue &key);
+ Q_INVOKABLE void clear();
+ Q_INVOKABLE QJSValue key(int index);
+
+ Q_INVOKABLE QJSValue get(const QJSValue &proxy, const QJSValue &key) const;
+ Q_INVOKABLE bool set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value);
+ Q_INVOKABLE bool has(const QJSValue &proxy, const QJSValue &key);
+ Q_INVOKABLE bool deleteProperty(const QJSValue &proxy, const QJSValue &key);
+ Q_INVOKABLE QJSValue keys(const QJSValue &proxy=0);
+ Q_INVOKABLE QJSValue enumerate();
+
+private:
+ static QString getStorageFileFor(const QString &storageDir, const QUuid &uuid);
+
+private:
+ QJSEngine *m_engine;
+ QSettings *m_storage;
+};
+
+#endif // JSKITLOCALSTORAGE_P_H
diff --git a/rockworkd/libpebble/jskit/jskitmanager.cpp b/rockworkd/libpebble/jskit/jskitmanager.cpp
new file mode 100644
index 0000000..04bf674
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitmanager.cpp
@@ -0,0 +1,240 @@
+#include <QFile>
+#include <QDir>
+#include <QUrl>
+
+#include "jskitmanager.h"
+#include "jskitpebble.h"
+
+JSKitManager::JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent) :
+ QObject(parent),
+ l(metaObject()->className()),
+ m_pebble(pebble),
+ m_connection(connection),
+ m_apps(apps),
+ m_appmsg(appmsg),
+ m_engine(0),
+ m_configurationUuid(0)
+{
+ connect(m_appmsg, &AppMsgManager::appStarted, this, &JSKitManager::handleAppStarted);
+ connect(m_appmsg, &AppMsgManager::appStopped, this, &JSKitManager::handleAppStopped);
+}
+
+JSKitManager::~JSKitManager()
+{
+ if (m_engine) {
+ stopJsApp();
+ }
+}
+
+QJSEngine * JSKitManager::engine()
+{
+ return m_engine;
+}
+
+bool JSKitManager::isJSKitAppRunning() const
+{
+ return m_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 (m_engine) {
+ qCDebug(l) << "requesting configuration";
+ m_jspebble->invokeCallbacks("showConfiguration");
+ } else {
+ qCWarning(l) << "requested to show configuration, but JS engine is not running";
+ }
+}
+
+void JSKitManager::handleWebviewClosed(const QString &result)
+{
+ if (m_engine) {
+ QJSValue eventObj = m_engine->newObject();
+ eventObj.setProperty("response", QUrl::fromPercentEncoding(result.toUtf8()));
+
+ qCDebug(l) << "Sending" << eventObj.property("response").toString();
+ m_jspebble->invokeCallbacks("webviewclosed", QJSValueList({eventObj}));
+
+ loadJsFile(":/cacheLocalStorage.js");
+ } else {
+ qCWarning(l) << "webview closed event, but JS engine is not running";
+ }
+}
+
+void JSKitManager::setConfigurationId(const QUuid &uuid)
+{
+ m_configurationUuid = uuid;
+}
+
+AppInfo JSKitManager::currentApp()
+{
+ return m_curApp;
+}
+
+void JSKitManager::handleAppStarted(const QUuid &uuid)
+{
+ AppInfo info = m_apps->info(uuid);
+ if (!info.uuid().isNull() && info.isJSKit()) {
+ qCDebug(l) << "Preparing to start JSKit app" << info.uuid() << info.shortName();
+
+ m_curApp = info;
+ startJsApp();
+ }
+}
+
+void JSKitManager::handleAppStopped(const QUuid &uuid)
+{
+ if (!m_curApp.uuid().isNull()) {
+ if (m_curApp.uuid() != uuid) {
+ qCWarning(l) << "Closed app with invalid UUID";
+ }
+
+ stopJsApp();
+ m_curApp = AppInfo();
+ qCDebug(l) << "App stopped" << uuid;
+ }
+}
+
+void JSKitManager::handleAppMessage(const QUuid &uuid, const QVariantMap &msg)
+{
+ if (m_curApp.uuid() == uuid) {
+ qCDebug(l) << "handling app message" << uuid << msg;
+
+ if (m_engine) {
+ QJSValue eventObj = m_engine->newObject();
+ eventObj.setProperty("payload", m_engine->toScriptValue(msg));
+
+ m_jspebble->invokeCallbacks("appmessage", QJSValueList({eventObj}));
+
+ loadJsFile(":/cacheLocalStorage.js");
+ }
+ else {
+ qCDebug(l) << "but engine is stopped";
+ }
+ }
+}
+
+bool JSKitManager::loadJsFile(const QString &filename)
+{
+ Q_ASSERT(m_engine);
+
+ QFile file(filename);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qCWarning(l) << "Failed to load JS file:" << file.fileName();
+ return false;
+ }
+
+ qCDebug(l) << "evaluating js file" << file.fileName();
+
+ QJSValue result = m_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 (m_engine) stopJsApp();
+
+ if (m_curApp.uuid().isNull()) {
+ qCWarning(l) << "Attempting to start JS app with invalid UUID";
+ return;
+ }
+
+ m_engine = new QJSEngine(this);
+ m_jspebble = new JSKitPebble(m_curApp, this, m_engine);
+ m_jsconsole = new JSKitConsole(m_engine);
+ m_jsstorage = new JSKitLocalStorage(m_engine, m_pebble->storagePath(), m_curApp.uuid());
+ m_jsgeo = new JSKitGeolocation(m_engine);
+ m_jstimer = new JSKitTimer(m_engine);
+ m_jsperformance = new JSKitPerformance(m_engine);
+
+ qCDebug(l) << "starting JS app" << m_curApp.shortName();
+
+ QJSValue globalObj = m_engine->globalObject();
+ QJSValue jskitObj = m_engine->newObject();
+
+ jskitObj.setProperty("pebble", m_engine->newQObject(m_jspebble));
+ jskitObj.setProperty("console", m_engine->newQObject(m_jsconsole));
+ jskitObj.setProperty("localstorage", m_engine->newQObject(m_jsstorage));
+ jskitObj.setProperty("geolocation", m_engine->newQObject(m_jsgeo));
+ jskitObj.setProperty("timer", m_engine->newQObject(m_jstimer));
+ jskitObj.setProperty("performance", m_engine->newQObject(m_jsperformance));
+ globalObj.setProperty("_jskit", jskitObj);
+
+ QJSValue navigatorObj = m_engine->newObject();
+ navigatorObj.setProperty("language", m_engine->toScriptValue(QLocale().name()));
+ globalObj.setProperty("navigator", navigatorObj);
+
+ // Set this.window = this
+ globalObj.setProperty("window", globalObj);
+
+ // Shims for compatibility...
+ loadJsFile(":/jskitsetup.js");
+
+ // Polyfills...
+ loadJsFile(":/typedarray.js");
+
+ // Now the actual script
+ QString jsApp = m_curApp.file(AppInfo::FileTypeJsApp, HardwarePlatformUnknown);
+ QFile f(jsApp);
+ if (!f.open(QFile::ReadOnly)) {
+ qCWarning(l) << "Error opening" << jsApp;
+ return;
+ }
+ QJSValue ret = m_engine->evaluate(QString::fromUtf8(f.readAll()));
+ qCDebug(l) << "loaded script" << ret.toString();
+
+ // Setup the message callback
+ QUuid uuid = m_curApp.uuid();
+ m_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...
+ m_jspebble->invokeCallbacks("ready");
+
+ loadJsFile(":/cacheLocalStorage.js");
+
+ if (m_configurationUuid == m_curApp.uuid()) {
+ qCDebug(l) << "going to launch config for" << m_configurationUuid;
+ showConfiguration();
+ }
+
+ m_configurationUuid = QUuid();
+}
+
+void JSKitManager::stopJsApp()
+{
+ qCDebug(l) << "stop js app" << m_curApp.uuid();
+ if (!m_engine) return; // Nothing to do!
+
+ loadJsFile(":/cacheLocalStorage.js");
+
+ if (!m_curApp.uuid().isNull()) {
+ m_appmsg->clearMessageHandler(m_curApp.uuid());
+ }
+
+ m_engine->collectGarbage();
+ m_engine->deleteLater();
+ m_engine = 0;
+}
diff --git a/rockworkd/libpebble/jskit/jskitmanager.h b/rockworkd/libpebble/jskit/jskitmanager.h
new file mode 100644
index 0000000..570948e
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitmanager.h
@@ -0,0 +1,72 @@
+#ifndef JSKITMANAGER_H
+#define JSKITMANAGER_H
+
+#include <QJSEngine>
+#include <QPointer>
+#include <QLoggingCategory>
+
+#include "../appmanager.h"
+#include "../watchconnection.h"
+#include "../pebble.h"
+#include "../appmsgmanager.h"
+
+#include "jskitconsole.h"
+#include "jskitgeolocation.h"
+#include "jskitlocalstorage.h"
+#include "jskittimer.h"
+#include "jskitperformance.h"
+
+class JSKitPebble;
+
+class JSKitManager : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent = 0);
+ ~JSKitManager();
+
+ QJSEngine * engine();
+ bool isJSKitAppRunning() const;
+
+ static QString describeError(QJSValue error);
+
+ void showConfiguration();
+ void handleWebviewClosed(const QString &result);
+ void setConfigurationId(const QUuid &uuid);
+ AppInfo currentApp();
+
+signals:
+ void appNotification(const QUuid &uuid, const QString &title, const QString &body);
+ void openURL(const QString &uuid, const QString &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;
+
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ AppManager *m_apps;
+ AppMsgManager *m_appmsg;
+ AppInfo m_curApp;
+ QJSEngine *m_engine;
+ QPointer<JSKitPebble> m_jspebble;
+ QPointer<JSKitConsole> m_jsconsole;
+ QPointer<JSKitLocalStorage> m_jsstorage;
+ QPointer<JSKitGeolocation> m_jsgeo;
+ QPointer<JSKitTimer> m_jstimer;
+ QPointer<JSKitPerformance> m_jsperformance;
+ QUuid m_configurationUuid;
+};
+
+#endif // JSKITMANAGER_H
diff --git a/rockworkd/libpebble/jskit/jskitpebble.cpp b/rockworkd/libpebble/jskit/jskitpebble.cpp
new file mode 100644
index 0000000..a300aef
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitpebble.cpp
@@ -0,0 +1,355 @@
+#include <QUrl>
+#include <QCryptographicHash>
+#include <QSettings>
+
+#include "jskitpebble.h"
+#include "jskitxmlhttprequest.h"
+
+static const char *token_salt = "0feeb7416d3c4546a19b04bccd8419b1";
+
+JSKitPebble::JSKitPebble(const AppInfo &info, JSKitManager *mgr, QObject *parent) :
+ QObject(parent),
+ l(metaObject()->className()),
+ m_appInfo(info),
+ m_mgr(mgr)
+{
+}
+
+void JSKitPebble::addEventListener(const QString &type, QJSValue function)
+{
+ m_listeners[type].append(function);
+}
+
+void JSKitPebble::removeEventListener(const QString &type, QJSValue function)
+{
+ if (!m_listeners.contains(type)) return;
+
+ QList<QJSValue> &callbacks = m_listeners[type];
+ for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ) {
+ if (it->strictlyEquals(function)) {
+ it = callbacks.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ if (callbacks.empty()) {
+ m_listeners.remove(type);
+ }
+}
+
+void JSKitPebble::showSimpleNotificationOnPebble(const QString &title, const QString &body)
+{
+ qCDebug(l) << "showSimpleNotificationOnPebble" << title << body;
+ emit m_mgr->appNotification(m_appInfo.uuid(), title, body);
+}
+
+uint JSKitPebble::sendAppMessage(QJSValue message, QJSValue callbackForAck, QJSValue callbackForNack)
+{
+ QVariantMap data = message.toVariant().toMap();
+ QPointer<JSKitPebble> pebbObj = this;
+ uint transactionId = m_mgr->m_appmsg->nextTransactionId();
+
+ qCDebug(l) << "sendAppMessage" << data;
+
+ m_mgr->m_appmsg->send(
+ m_appInfo.uuid(),
+ data,
+ [this, pebbObj, transactionId, callbackForAck]() mutable {
+ if (pebbObj.isNull()) return;
+
+ if (callbackForAck.isCallable()) {
+ QJSValue event = pebbObj->buildAckEventObject(transactionId);
+ QJSValue result = callbackForAck.call(QJSValueList({event}));
+
+ if (result.isError()) {
+ qCWarning(l) << "error while invoking ACK callback"
+ << callbackForAck.toString() << ":"
+ << JSKitManager::describeError(result);
+ }
+ }
+ },
+ [this, pebbObj, transactionId, callbackForNack]() mutable {
+ if (pebbObj.isNull()) return;
+
+ if (callbackForNack.isCallable()) {
+ QJSValue event = pebbObj->buildAckEventObject(transactionId, "NACK from watch");
+ QJSValue result = callbackForNack.call(QJSValueList({event}));
+
+ if (result.isError()) {
+ qCWarning(l) << "error while invoking NACK callback"
+ << callbackForNack.toString() << ":"
+ << JSKitManager::describeError(result);
+ }
+ }
+ }
+ );
+
+ return transactionId;
+}
+
+void JSKitPebble::getTimelineToken(QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.getTimelineToken";
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+void JSKitPebble::timelineSubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.timelineSubscribe";
+ Q_UNUSED(topic);
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+void JSKitPebble::timelineUnsubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.timelineUnsubscribe";
+ Q_UNUSED(topic);
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+void JSKitPebble::timelineSubscriptions(QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.timelineSubscriptions";
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+
+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(m_appInfo.uuid().toByteArray());
+
+ QSettings settings;
+ QString token = settings.value("accountToken").toString();
+
+ if (token.isEmpty()) {
+ token = QUuid::createUuid().toString();
+ qCDebug(l) << "created new account token" << token;
+ settings.setValue("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(m_appInfo.uuid().toByteArray());
+ hasher.addData(m_mgr->m_pebble->serialNumber().toLatin1());
+
+ QString hash = hasher.result().toHex();
+ qCDebug(l) << "returning watch token" << hash;
+
+ return hash;
+}
+
+QJSValue JSKitPebble::getActiveWatchInfo() const
+{
+ QJSValue watchInfo = m_mgr->m_engine->newObject();
+
+ switch (m_mgr->m_pebble->hardwarePlatform()) {
+ case HardwarePlatformBasalt:
+ watchInfo.setProperty("platform", "basalt");
+ break;
+
+ case HardwarePlatformChalk:
+ watchInfo.setProperty("platform", "chalk");
+ break;
+
+ default:
+ watchInfo.setProperty("platform", "aplite");
+ break;
+ }
+
+ switch (m_mgr->m_pebble->model()) {
+ case ModelTintinWhite:
+ watchInfo.setProperty("model", "pebble_white");
+ break;
+
+ case ModelTintinRed:
+ watchInfo.setProperty("model", "pebble_red");
+ break;
+
+ case ModelTintinOrange:
+ watchInfo.setProperty("model", "pebble_orange");
+ break;
+
+ case ModelTintinGrey:
+ watchInfo.setProperty("model", "pebble_grey");
+ break;
+
+ case ModelBiancaSilver:
+ watchInfo.setProperty("model", "pebble_steel_silver");
+ break;
+
+ case ModelBiancaBlack:
+ watchInfo.setProperty("model", "pebble_steel_black");
+ break;
+
+ case ModelTintinBlue:
+ watchInfo.setProperty("model", "pebble_blue");
+ break;
+
+ case ModelTintinGreen:
+ watchInfo.setProperty("model", "pebble_green");
+ break;
+
+ case ModelTintinPink:
+ watchInfo.setProperty("model", "pebble_pink");
+ break;
+
+ case ModelSnowyWhite:
+ watchInfo.setProperty("model", "pebble_time_white");
+ break;
+
+ case ModelSnowyBlack:
+ watchInfo.setProperty("model", "pebble_time_black");
+ break;
+
+ case ModelSnowyRed:
+ watchInfo.setProperty("model", "pebble_time_read");
+ break;
+
+ case ModelBobbySilver:
+ watchInfo.setProperty("model", "pebble_time_steel_silver");
+ break;
+
+ case ModelBobbyBlack:
+ watchInfo.setProperty("model", "pebble_time_steel_black");
+ break;
+
+ case ModelBobbyGold:
+ watchInfo.setProperty("model", "pebble_time_steel_gold");
+ break;
+
+ case ModelSpalding14Silver:
+ watchInfo.setProperty("model", "pebble_time_round_silver_14mm");
+ break;
+
+ case ModelSpalding14Black:
+ watchInfo.setProperty("model", "pebble_time_round_black_14mm");
+ break;
+
+ case ModelSpalding20Silver:
+ watchInfo.setProperty("model", "pebble_time_round_silver_20mm");
+ break;
+
+ case ModelSpalding20Black:
+ watchInfo.setProperty("model", "pebble_time_round_black_20mm");
+ break;
+
+ case ModelSpalding14RoseGold:
+ watchInfo.setProperty("model", "pebble_time_round_rose_gold_14mm");
+ break;
+
+ default:
+ watchInfo.setProperty("model", "pebble_black");
+ break;
+ }
+
+ watchInfo.setProperty("language", m_mgr->m_pebble->language());
+
+ QJSValue firmware = m_mgr->m_engine->newObject();
+ QString version = m_mgr->m_pebble->softwareVersion().remove("v");
+ QStringList versionParts = version.split(".");
+
+ if (versionParts.count() >= 1) {
+ firmware.setProperty("major", versionParts[0].toInt());
+ }
+
+ if (versionParts.count() >= 2) {
+ firmware.setProperty("minor", versionParts[1].toInt());
+ }
+
+ if (versionParts.count() >= 3) {
+ if (versionParts[2].contains("-")) {
+ QStringList patchParts = version.split("-");
+ firmware.setProperty("patch", patchParts[0].toInt());
+ firmware.setProperty("suffix", patchParts[1]);
+ } else {
+ firmware.setProperty("patch", versionParts[2].toInt());
+ firmware.setProperty("suffix", "");
+ }
+ }
+
+ watchInfo.setProperty("firmware", firmware);
+ return watchInfo;
+}
+
+void JSKitPebble::openURL(const QUrl &url)
+{
+ emit m_mgr->openURL(m_appInfo.uuid().toString(), url.toString());
+}
+
+QJSValue JSKitPebble::createXMLHttpRequest()
+{
+ JSKitXMLHttpRequest *xhr = new JSKitXMLHttpRequest(m_mgr->engine());
+ // Should be deleted by JS engine.
+ return m_mgr->engine()->newQObject(xhr);
+}
+
+QJSValue JSKitPebble::buildAckEventObject(uint transaction, const QString &message) const
+{
+ QJSEngine *engine = m_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 (!m_listeners.contains(type)) return;
+ QList<QJSValue> &callbacks = m_listeners[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);
+ }
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskitpebble.h b/rockworkd/libpebble/jskit/jskitpebble.h
new file mode 100644
index 0000000..d9cd670
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitpebble.h
@@ -0,0 +1,47 @@
+#ifndef JSKITPEBBLE_P_H
+#define JSKITPEBBLE_P_H
+
+#include <QLoggingCategory>
+
+#include "jskitmanager.h"
+#include "../appinfo.h"
+
+class JSKitPebble : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitPebble(const AppInfo &appInfo, JSKitManager *mgr, QObject *parent=0);
+
+ Q_INVOKABLE void addEventListener(const QString &type, QJSValue function);
+ Q_INVOKABLE void removeEventListener(const QString &type, QJSValue function);
+
+ Q_INVOKABLE void showSimpleNotificationOnPebble(const QString &title, const QString &body);
+ Q_INVOKABLE uint sendAppMessage(QJSValue message, QJSValue callbackForAck = QJSValue(), QJSValue callbackForNack = QJSValue());
+
+ Q_INVOKABLE void getTimelineToken(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+ Q_INVOKABLE void timelineSubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+ Q_INVOKABLE void timelineUnsubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+ Q_INVOKABLE void timelineSubscriptions(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+
+ Q_INVOKABLE QString getAccountToken() const;
+ Q_INVOKABLE QString getWatchToken() const;
+ Q_INVOKABLE QJSValue getActiveWatchInfo() const;
+
+ Q_INVOKABLE void openURL(const QUrl &url);
+
+ 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 m_appInfo;
+ JSKitManager *m_mgr;
+ QHash<QString, QList<QJSValue>> m_listeners;
+};
+
+#endif // JSKITPEBBLE_P_H
diff --git a/rockworkd/libpebble/jskit/jskitperformance.cpp b/rockworkd/libpebble/jskit/jskitperformance.cpp
new file mode 100644
index 0000000..23b0e08
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitperformance.cpp
@@ -0,0 +1,13 @@
+#include "jskitperformance.h"
+
+JSKitPerformance::JSKitPerformance(QObject *parent) :
+ QObject(parent),
+ m_start(QTime::currentTime())
+{
+}
+
+int JSKitPerformance::now()
+{
+ QTime now = QTime::currentTime();
+ return m_start.msecsTo(now);
+}
diff --git a/rockworkd/libpebble/jskit/jskitperformance.h b/rockworkd/libpebble/jskit/jskitperformance.h
new file mode 100644
index 0000000..5f118be
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitperformance.h
@@ -0,0 +1,20 @@
+#ifndef JSKITPERFORMANCE_H
+#define JSKITPERFORMANCE_H
+
+#include <QObject>
+#include <QTime>
+
+class JSKitPerformance : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit JSKitPerformance(QObject *parent=0);
+
+ Q_INVOKABLE int now();
+
+private:
+ QTime m_start;
+};
+
+#endif // JSKITPERFORMANCE_H
diff --git a/rockworkd/libpebble/jskit/jskitsetup.js b/rockworkd/libpebble/jskit/jskitsetup.js
new file mode 100644
index 0000000..340c4f1
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitsetup.js
@@ -0,0 +1,196 @@
+//Borrowed from https://github.com/pebble/pypkjs/blob/master/pypkjs/javascript/runtime.py#L17
+_jskit.make_proxies = function(proxy, origin, names) {
+ names.forEach(function(name) {
+ proxy[name] = eval("(function " + name + "() { return origin[name].apply(origin, arguments); })");
+ });
+
+ return proxy;
+}
+
+_jskit.make_properties = function(proxy, origin, names) {
+ names.forEach(function(name) {
+ Object.defineProperty(proxy, name, {
+ configurable: false,
+ enumerable: true,
+ get: function() {
+ return origin[name];
+ },
+ set: function(value) {
+ origin[name] = value;
+ }
+ });
+ });
+
+ return proxy;
+}
+
+Pebble = new (function() {
+ _jskit.make_proxies(this, _jskit.pebble,
+ ['sendAppMessage', 'showSimpleNotificationOnPebble', 'getAccountToken', 'getWatchToken',
+ 'addEventListener', 'removeEventListener', 'openURL', 'getTimelineToken', 'timelineSubscribe',
+ 'timelineUnsubscribe', 'timelineSubscriptions', 'getActiveWatchInfo']
+ );
+})();
+
+performance = new (function() {
+ _jskit.make_proxies(this, _jskit.performance, ['now']);
+})();
+
+function XMLHttpRequest() {
+ var xhr = _jskit.pebble.createXMLHttpRequest();
+ _jskit.make_proxies(this, xhr,
+ ['open', 'setRequestHeader', 'overrideMimeType', 'send', 'getResponseHeader',
+ 'getAllResponseHeaders', 'abort', 'addEventListener', 'removeEventListener']);
+ _jskit.make_properties(this, xhr,
+ ['readyState', 'response', 'responseText', 'responseType', 'status',
+ 'statusText', 'timeout', 'onreadystatechange', 'ontimeout', 'onload',
+ 'onloadstart', 'onloadend', 'onprogress', 'onerror', 'onabort']);
+
+ this.UNSENT = 0;
+ this.OPENED = 1;
+ this.HEADERS_RECEIVED = 2;
+ this.LOADING = 3;
+ this.DONE = 4;
+}
+
+function setInterval(func, time) {
+ return _jskit.timer.setInterval(func, time);
+}
+
+function clearInterval(id) {
+ _jskit.timer.clearInterval(id);
+}
+
+function setTimeout(func, time) {
+ return _jskit.timer.setTimeout(func, time);
+}
+
+function clearTimeout(id) {
+ _jskit.timer.clearTimeout(id);
+}
+
+navigator.geolocation = new (function() {
+ _jskit.make_proxies(this, _jskit.geolocation,
+ ['getCurrentPosition', 'watchPosition', 'clearWatch']
+ );
+})();
+
+console = new (function() {
+ _jskit.make_proxies(this, _jskit.console,
+ ['log', 'warn', 'error', 'info']
+ );
+})();
+
+/*localStorage = new (function() {
+ _jskit.make_proxies(this, _jskit.localstorage,
+ ['clear', 'getItem', 'setItem', 'removeItem', 'key']
+ );
+
+ _jskit.make_properties(this, _jskit.localstorage,
+ ['length']
+ );
+})();*/
+
+//It appears that Proxy is not available since Qt is using Javascript v5
+/*(function() {
+ var proxy = _jskit.make_proxies({}, _jskit.localstorage, ['set', 'has', 'deleteProperty', 'keys', 'enumerate']);
+ var methods = _jskit.make_proxies({}, _jskit.localstorage, ['clear', 'getItem', 'setItem', 'removeItem', 'key']);
+ proxy.get = function get(p, name) { return methods[name] || _jskit.localstorage.get(p, name); }
+ this.localStorage = Proxy.create(proxy);
+})();*/
+
+//inspired by https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage
+Object.defineProperty(window, "localStorage", new (function () {
+ var storage = {};
+ Object.defineProperty(storage, "getItem", {
+ value: function (key) {
+ var value = null;
+ if (key !== undefined && key !== null && storage[key] !== undefined) {
+ value = storage[key];
+ }
+
+ return value;
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "key", {
+ value: function (index) {
+ return Object.keys(storage)[index];
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "setItem", {
+ value: function (key, value) {
+ if (key !== undefined && key !== null) {
+ _jskit.localstorage.setItem(key, value);
+ storage[key] = (value && value.toString) ? value.toString() : value;
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "length", {
+ get: function () {
+ return Object.keys(storage).length;
+ },
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "removeItem", {
+ value: function (key) {
+ if (key && storage[key]) {
+ _jskit.localstorage.removeItem(key);
+ delete storage[key];
+
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "clear", {
+ value: function (key) {
+ for (var key in storage) {
+ storage.removeItem(key);
+ }
+
+ return true;
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ this.get = function () {
+ return storage;
+ };
+
+ this.configurable = false;
+ this.enumerable = true;
+})());
+
+(function() {
+ var keys = _jskit.localstorage.keys();
+ for (var index in keys) {
+ var value = _jskit.localstorage.getItem(keys[index]);
+ localStorage.setItem(keys[index], value);
+ }
+})();
diff --git a/rockworkd/libpebble/jskit/jskittimer.cpp b/rockworkd/libpebble/jskit/jskittimer.cpp
new file mode 100644
index 0000000..6ab5b4a
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskittimer.cpp
@@ -0,0 +1,77 @@
+#include <QTimerEvent>
+
+#include "jskittimer.h"
+
+JSKitTimer::JSKitTimer(QJSEngine *engine) :
+ QObject(engine),
+ l(metaObject()->className()),
+ m_engine(engine)
+{
+}
+
+int JSKitTimer::setInterval(QJSValue expression, int delay) //TODO support optional parameters
+{
+ qCDebug(l) << "Setting interval for " << delay << "ms: " << expression.toString();
+
+ if (expression.isString() || expression.isCallable()) {
+ int timerId = startTimer(delay);
+ m_intervals.insert(timerId, expression);
+
+ return timerId;
+ }
+
+ return -1;
+}
+
+void JSKitTimer::clearInterval(int timerId)
+{
+ qCDebug(l) << "Killing interval " << timerId ;
+ killTimer(timerId);
+ m_intervals.remove(timerId);
+}
+
+int JSKitTimer::setTimeout(QJSValue expression, int delay) //TODO support optional parameters
+{
+ qCDebug(l) << "Setting timeout for " << delay << "ms: " << expression.toString();
+
+ if (expression.isString() || expression.isCallable()) {
+ int timerId = startTimer(delay);
+ m_timeouts.insert(timerId, expression);
+
+ return timerId;
+ }
+
+ return -1;
+}
+
+void JSKitTimer::clearTimeout(int timerId)
+{
+ qCDebug(l) << "Killing timeout " << timerId ;
+ killTimer(timerId);
+ m_timeouts.remove(timerId);
+}
+
+void JSKitTimer::timerEvent(QTimerEvent *event)
+{
+ int id = event->timerId();
+
+ QJSValue expression; // find in either intervals or timeouts
+ if (m_intervals.contains(id)) {
+ expression = m_intervals.value(id);
+ } else if (m_timeouts.contains(id)) {
+ expression = m_timeouts.value(id);
+ killTimer(id); // timeouts don't repeat
+ } else {
+ qCWarning(l) << "Unknown timer event";
+ killTimer(id); // interval nor timeout exist. kill the timer
+
+ return;
+ }
+
+ if (expression.isCallable()) { // call it if it's a function
+ expression.call().toString();
+ }
+ else { // otherwise evaluate it
+ m_engine->evaluate(expression.toString());
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskittimer.h b/rockworkd/libpebble/jskit/jskittimer.h
new file mode 100644
index 0000000..50b394d
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskittimer.h
@@ -0,0 +1,31 @@
+#ifndef JSKITTIMER_P_H
+#define JSKITTIMER_P_H
+
+#include <QLoggingCategory>
+#include <QJSValue>
+#include <QJSEngine>
+
+class JSKitTimer : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitTimer(QJSEngine *engine);
+
+ Q_INVOKABLE int setInterval(QJSValue expression, int delay);
+ Q_INVOKABLE void clearInterval(int timerId);
+
+ Q_INVOKABLE int setTimeout(QJSValue expression, int delay);
+ Q_INVOKABLE void clearTimeout(int timerId);
+
+protected:
+ void timerEvent(QTimerEvent *event);
+
+private:
+ QJSEngine *m_engine;
+ QHash<int, QJSValue> m_intervals;
+ QHash<int, QJSValue> m_timeouts;
+};
+
+#endif // JSKITTIMER_P_H
diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp
new file mode 100644
index 0000000..5948683
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp
@@ -0,0 +1,318 @@
+#include <QBuffer>
+#include <QAuthenticator>
+#include <QEventLoop>
+
+#include "jskitxmlhttprequest.h"
+#include "jskitmanager.h"
+
+JSKitXMLHttpRequest::JSKitXMLHttpRequest(QJSEngine *engine) :
+ QObject(engine),
+ l(metaObject()->className()),
+ m_engine(engine),
+ m_net(new QNetworkAccessManager(this)),
+ m_timeout(0),
+ m_reply(0)
+{
+ connect(m_net, &QNetworkAccessManager::authenticationRequired,
+ this, &JSKitXMLHttpRequest::handleAuthenticationRequired);
+}
+
+void JSKitXMLHttpRequest::open(const QString &method, const QString &url, bool async, const QString &username, const QString &password)
+{
+ if (m_reply) {
+ m_reply->deleteLater();
+ m_reply = 0;
+ }
+
+ m_username = username;
+ m_password = password;
+ m_request = QNetworkRequest(QUrl(url));
+ m_verb = method;
+ m_async = async;
+
+ qCDebug(l) << "opened to URL" << m_request.url().toString() << "Async:" << async;
+}
+
+void JSKitXMLHttpRequest::setRequestHeader(const QString &header, const QString &value)
+{
+ qCDebug(l) << "setRequestHeader" << header << value;
+ m_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" << m_verb << "to" << m_request.url() << "with" << QString::fromUtf8(byteData);
+ m_reply = m_net->sendCustomRequest(m_request, m_verb.toLatin1(), buffer);
+
+ connect(m_reply, &QNetworkReply::finished,
+ this, &JSKitXMLHttpRequest::handleReplyFinished);
+ connect(m_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(m_reply);
+ }
+
+ if (!m_async) {
+ QEventLoop loop; //Hacky way to get QNetworkReply be synchronous
+
+ connect(m_reply, &QNetworkReply::finished,
+ &loop, &QEventLoop::quit);
+ connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
+ &loop, &QEventLoop::quit);
+
+ loop.exec();
+ }
+}
+
+void JSKitXMLHttpRequest::abort()
+{
+ if (m_reply) {
+ m_reply->deleteLater();
+ m_reply = 0;
+ }
+}
+
+QJSValue JSKitXMLHttpRequest::onload() const
+{
+ return m_onload;
+}
+
+void JSKitXMLHttpRequest::setOnload(const QJSValue &value)
+{
+ m_onload = value;
+}
+
+QJSValue JSKitXMLHttpRequest::onreadystatechange() const
+{
+ return m_onreadystatechange;
+}
+
+void JSKitXMLHttpRequest::setOnreadystatechange(const QJSValue &value)
+{
+ m_onreadystatechange = value;
+}
+
+QJSValue JSKitXMLHttpRequest::ontimeout() const
+{
+ return m_ontimeout;
+}
+
+void JSKitXMLHttpRequest::setOntimeout(const QJSValue &value)
+{
+ m_ontimeout = value;
+}
+
+QJSValue JSKitXMLHttpRequest::onerror() const
+{
+ return m_onerror;
+}
+
+void JSKitXMLHttpRequest::setOnerror(const QJSValue &value)
+{
+ m_onerror = value;
+}
+
+uint JSKitXMLHttpRequest::readyState() const
+{
+ if (!m_reply) {
+ return UNSENT;
+ } else if (m_reply->isFinished()) {
+ return DONE;
+ } else {
+ return LOADING;
+ }
+}
+
+uint JSKitXMLHttpRequest::timeout() const
+{
+ return m_timeout;
+}
+
+void JSKitXMLHttpRequest::setTimeout(uint value)
+{
+ m_timeout = value;
+ // TODO Handle fetch in-progress.
+}
+
+uint JSKitXMLHttpRequest::status() const
+{
+ if (!m_reply || !m_reply->isFinished()) {
+ return 0;
+ } else {
+ return m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt();
+ }
+}
+
+QString JSKitXMLHttpRequest::statusText() const
+{
+ if (!m_reply || !m_reply->isFinished()) {
+ return QString();
+ } else {
+ return m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+ }
+}
+
+QString JSKitXMLHttpRequest::responseType() const
+{
+ return m_responseType;
+}
+
+void JSKitXMLHttpRequest::setResponseType(const QString &type)
+{
+ qCDebug(l) << "response type set to" << type;
+ m_responseType = type;
+}
+
+QJSValue JSKitXMLHttpRequest::response() const
+{
+ if (m_responseType.isEmpty() || m_responseType == "text") {
+ return m_engine->toScriptValue(QString::fromUtf8(m_response));
+ } else if (m_responseType == "arraybuffer") {
+ QJSValue arrayBufferProto = m_engine->globalObject().property("ArrayBuffer").property("prototype");
+ QJSValue arrayBuf = m_engine->newObject();
+
+ if (!arrayBufferProto.isUndefined()) {
+ arrayBuf.setPrototype(arrayBufferProto);
+ arrayBuf.setProperty("byteLength", m_engine->toScriptValue<uint>(m_response.size()));
+
+ QJSValue array = m_engine->newArray(m_response.size());
+ for (int i = 0; i < m_response.size(); i++) {
+ array.setProperty(i, m_engine->toScriptValue<int>(m_response[i]));
+ }
+
+ arrayBuf.setProperty("_bytes", array);
+ qCDebug(l) << "returning ArrayBuffer of" << m_response.size() << "bytes";
+ } else {
+ qCWarning(l) << "Cannot find proto of ArrayBuffer";
+ }
+
+ return arrayBuf;
+ } else {
+ qCWarning(l) << "unsupported responseType:" << m_responseType;
+ return m_engine->toScriptValue<void*>(0);
+ }
+}
+
+QString JSKitXMLHttpRequest::responseText() const
+{
+ return QString::fromUtf8(m_response);
+}
+
+void JSKitXMLHttpRequest::handleReplyFinished()
+{
+ if (!m_reply) {
+ qCDebug(l) << "reply finished too late";
+ return;
+ }
+
+ m_response = m_reply->readAll();
+ qCDebug(l) << "reply finished, reply text:" << QString::fromUtf8(m_response) << "status:" << status();
+
+ emit readyStateChanged();
+ emit statusChanged();
+ emit statusTextChanged();
+ emit responseChanged();
+ emit responseTextChanged();
+
+ if (m_onload.isCallable()) {
+ qCDebug(l) << "going to call onload handler:" << m_onload.toString();
+
+ QJSValue result = m_onload.callWithInstance(m_engine->newQObject(this));
+ if (result.isError()) {
+ qCWarning(l) << "JS error on onload handler:" << JSKitManager::describeError(result);
+ }
+ } else {
+ qCDebug(l) << "No onload set";
+ }
+
+ if (m_onreadystatechange.isCallable()) {
+ qCDebug(l) << "going to call onreadystatechange handler:" << m_onreadystatechange.toString();
+ QJSValue result = m_onreadystatechange.callWithInstance(m_engine->newQObject(this));
+ if (result.isError()) {
+ qCWarning(l) << "JS error on onreadystatechange handler:" << JSKitManager::describeError(result);
+ }
+ }
+}
+
+void JSKitXMLHttpRequest::handleReplyError(QNetworkReply::NetworkError code)
+{
+ if (!m_reply) {
+ qCDebug(l) << "reply error too late";
+ return;
+ }
+
+ qCDebug(l) << "reply error" << code;
+
+ emit readyStateChanged();
+ emit statusChanged();
+ emit statusTextChanged();
+
+ if (m_onerror.isCallable()) {
+ qCDebug(l) << "going to call onerror handler:" << m_onload.toString();
+ QJSValue result = m_onerror.callWithInstance(m_engine->newQObject(this));
+ if (result.isError()) {
+ qCWarning(l) << "JS error on onerror handler:" << JSKitManager::describeError(result);
+ }
+ }
+}
+
+void JSKitXMLHttpRequest::handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth)
+{
+ if (m_reply == reply) {
+ qCDebug(l) << "authentication required";
+
+ if (!m_username.isEmpty() || !m_password.isEmpty()) {
+ qCDebug(l) << "using provided authorization:" << m_username;
+
+ auth->setUser(m_username);
+ auth->setPassword(m_password);
+ } else {
+ qCDebug(l) << "no username or password provided";
+ }
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.h b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h
new file mode 100644
index 0000000..70b8136
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h
@@ -0,0 +1,96 @@
+#ifndef JSKITXMLHTTPREQUEST_P_H
+#define JSKITXMLHTTPREQUEST_P_H
+
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QJSEngine>
+#include <QLoggingCategory>
+
+class JSKitXMLHttpRequest : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+ Q_PROPERTY(QJSValue onload READ onload WRITE setOnload)
+ Q_PROPERTY(QJSValue onreadystatechange READ onreadystatechange WRITE setOnreadystatechange)
+ 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(QJSEngine *engine);
+
+ enum ReadyStates {
+ UNSENT = 0,
+ OPENED = 1,
+ HEADERS_RECEIVED = 2,
+ LOADING = 3,
+ DONE = 4
+ };
+ Q_ENUMS(ReadyStates)
+
+ Q_INVOKABLE void open(const QString &method, const QString &url, bool async = true, 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 onreadystatechange() const;
+ void setOnreadystatechange(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:
+ QJSEngine *m_engine;
+ QNetworkAccessManager *m_net;
+ QString m_verb;
+ bool m_async = true;
+ uint m_timeout;
+ QString m_username;
+ QString m_password;
+ QNetworkRequest m_request;
+ QNetworkReply *m_reply;
+ QString m_responseType;
+ QByteArray m_response;
+ QJSValue m_onload;
+ QJSValue m_onreadystatechange;
+ QJSValue m_ontimeout;
+ QJSValue m_onerror;
+};
+
+#endif // JSKITXMLHTTPREQUEST_P_H
diff --git a/rockworkd/libpebble/jskit/typedarray.js b/rockworkd/libpebble/jskit/typedarray.js
new file mode 100644
index 0000000..d4e00c6
--- /dev/null
+++ b/rockworkd/libpebble/jskit/typedarray.js
@@ -0,0 +1,1037 @@
+/*
+ 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; } //ROCKWORK HACK ALERT: it appears that QT doesn't do the >>> properly, using >> here instead (should be close enough)
+
+ // 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 & 0xff, (n >> 8) & 0xff]; }
+ function unpackI16(bytes) { return as_signed(bytes[1] << 8 | bytes[0], 16); }
+
+ function packU16(n) { return [n & 0xff, (n >> 8) & 0xff]; }
+ function unpackU16(bytes) { return as_unsigned(bytes[1] << 8 | bytes[0], 16); }
+
+ function packI32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; }
+ function unpackI32(bytes) { return as_signed(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); }
+
+ function packU32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; }
+ function unpackU32(bytes) { return as_unsigned(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); }
+
+ function packIEEE754(v, ebits, fbits) {
+
+ var bias = (1 << (ebits - 1)) - 1;
+
+ 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
+ var s, e, f;
+ 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)) {
+ // Normalized
+ e = min(floor(log(v) / LN2), 1023);
+ var significand = v / pow(2, e);
+ if (significand < 1) {
+ e -= 1;
+ significand *= 2;
+ }
+ if (significand >= 2) {
+ e += 1;
+ significand /= 2;
+ }
+ var d = pow(2, fbits);
+ f = roundToEven(significand * d) - d;
+ e += bias;
+ if (f / d >= 1) {
+ e += 1;
+ f = 0;
+ }
+ if (e > 2 * bias) {
+ // Overflow
+ e = (1 << ebits) - 1;
+ f = 0;
+ }
+ } else {
+ // Denormalized
+ e = 0;
+ f = roundToEven(v / pow(2, 1 - bias - fbits));
+ }
+ }
+
+ // Pack sign, exponent, fraction
+ var bits = [], i;
+ 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();
+ var str = bits.join('');
+
+ // Bits to bytes
+ var bytes = [];
+ while (str.length) {
+ bytes.unshift(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 = 0; i < bytes.length; ++i) {
+ b = bytes[i];
+ 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/rockworkd/libpebble/musicendpoint.cpp b/rockworkd/libpebble/musicendpoint.cpp
new file mode 100644
index 0000000..f66afda
--- /dev/null
+++ b/rockworkd/libpebble/musicendpoint.cpp
@@ -0,0 +1,63 @@
+#include "musicendpoint.h"
+#include "pebble.h"
+#include "watchconnection.h"
+
+#include <QDebug>
+
+MusicEndpoint::MusicEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_watchConnection(connection)
+{
+ m_watchConnection->registerEndpointHandler(WatchConnection::EndpointMusicControl, this, "handleMessage");
+}
+
+void MusicEndpoint::setMusicMetadata(const MusicMetaData &metaData)
+{
+ m_metaData = metaData;
+ writeMetadata();
+}
+
+void MusicEndpoint::writeMetadata()
+{
+ if (!m_watchConnection->isConnected()) {
+ return;
+ }
+ QStringList tmp;
+ tmp.append(m_metaData.artist.left(30));
+ tmp.append(m_metaData.album.left(30));
+ tmp.append(m_metaData.title.left(30));
+ QByteArray res = m_watchConnection->buildMessageData(16, tmp); // Not yet sure what the 16 is about :/
+
+ m_watchConnection->writeToPebble(WatchConnection::EndpointMusicControl, res);
+}
+
+void MusicEndpoint::handleMessage(const QByteArray &data)
+{
+ MusicControlButton controlButton;
+ switch (data.toHex().toInt()) {
+ case 0x01:
+ controlButton = MusicControlPlayPause;
+ break;
+ case 0x04:
+ controlButton = MusicControlSkipNext;
+ break;
+ case 0x05:
+ controlButton = MusicControlSkipBack;
+ break;
+ case 0x06:
+ controlButton = MusicControlVolumeUp;
+ break;
+ case 0x07:
+ controlButton = MusicControlVolumeDown;
+ break;
+ case 0x08:
+ writeMetadata();
+ return;
+ default:
+ qWarning() << "Unhandled music control button pressed:" << data.toHex();
+ return;
+ }
+ emit musicControlPressed(controlButton);
+}
+
diff --git a/rockworkd/libpebble/musicendpoint.h b/rockworkd/libpebble/musicendpoint.h
new file mode 100644
index 0000000..1978e46
--- /dev/null
+++ b/rockworkd/libpebble/musicendpoint.h
@@ -0,0 +1,37 @@
+#ifndef MUSICENDPOINT_H
+#define MUSICENDPOINT_H
+
+#include "musicmetadata.h"
+#include "enums.h"
+
+#include <QObject>
+
+class Pebble;
+class WatchConnection;
+
+class MusicEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ explicit MusicEndpoint(Pebble *pebble, WatchConnection *connection);
+
+public slots:
+ void setMusicMetadata(const MusicMetaData &metaData);
+
+private slots:
+ void handleMessage(const QByteArray &data);
+
+signals:
+ void musicControlPressed(MusicControlButton button);
+
+private:
+ void writeMetadata();
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_watchConnection;
+
+ MusicMetaData m_metaData;
+};
+
+#endif // MUSICENDPOINT_H
diff --git a/rockworkd/libpebble/musicmetadata.cpp b/rockworkd/libpebble/musicmetadata.cpp
new file mode 100644
index 0000000..fd40a2a
--- /dev/null
+++ b/rockworkd/libpebble/musicmetadata.cpp
@@ -0,0 +1,14 @@
+#include "musicmetadata.h"
+
+MusicMetaData::MusicMetaData()
+{
+
+}
+
+MusicMetaData::MusicMetaData(const QString &artist, const QString &album, const QString &title):
+ artist(artist),
+ album(album),
+ title(title)
+{
+
+}
diff --git a/rockworkd/libpebble/musicmetadata.h b/rockworkd/libpebble/musicmetadata.h
new file mode 100644
index 0000000..d40872c
--- /dev/null
+++ b/rockworkd/libpebble/musicmetadata.h
@@ -0,0 +1,17 @@
+#ifndef MUSICMETADATA_H
+#define MUSICMETADATA_H
+
+#include <QString>
+
+class MusicMetaData
+{
+public:
+ MusicMetaData();
+ MusicMetaData(const QString &artist, const QString &album, const QString &title);
+
+ QString artist;
+ QString album;
+ QString title;
+};
+
+#endif // MUSICMETADATA_H
diff --git a/rockworkd/libpebble/notification.cpp b/rockworkd/libpebble/notification.cpp
new file mode 100644
index 0000000..4b149b8
--- /dev/null
+++ b/rockworkd/libpebble/notification.cpp
@@ -0,0 +1,79 @@
+#include "notification.h"
+
+Notification::Notification(const QString &sourceId) :
+ m_sourceId(sourceId)
+{
+
+}
+
+QString Notification::sourceId() const
+{
+ return m_sourceId;
+}
+
+void Notification::setSourceId(const QString &sourceId)
+{
+ m_sourceId = sourceId;
+}
+
+QString Notification::sourceName() const
+{
+ return m_sourceName;
+}
+
+void Notification::setSourceName(const QString &sourceName)
+{
+ m_sourceName = sourceName;
+}
+
+QString Notification::sender() const
+{
+ return m_sender;
+}
+
+void Notification::setSender(const QString &sender)
+{
+ m_sender = sender;
+}
+
+QString Notification::subject() const
+{
+ return m_subject;
+}
+
+void Notification::setSubject(const QString &subject)
+{
+ m_subject = subject;
+}
+
+QString Notification::body() const
+{
+ return m_body;
+}
+
+void Notification::setBody(const QString &body)
+{
+ m_body = body;
+}
+
+Notification::NotificationType Notification::type() const
+{
+ return m_type;
+}
+
+void Notification::setType(Notification::NotificationType type)
+{
+ m_type = type;
+}
+
+QString Notification::actToken() const
+{
+ return m_actToken;
+}
+
+void Notification::setActToken(QString actToken)
+{
+ m_actToken = actToken;
+}
+
+
diff --git a/rockworkd/libpebble/notification.h b/rockworkd/libpebble/notification.h
new file mode 100644
index 0000000..1ab76a0
--- /dev/null
+++ b/rockworkd/libpebble/notification.h
@@ -0,0 +1,59 @@
+#ifndef NOTIFICATION_H
+#define NOTIFICATION_H
+
+#include <QString>
+
+class Notification
+{
+public:
+ enum NotificationType {
+ NotificationTypeGeneric,
+ NotificationTypeEmail,
+ NotificationTypeSMS,
+ NotificationTypeFacebook,
+ NotificationTypeTwitter,
+ NotificationTypeTelegram,
+ NotificationTypeWhatsApp,
+ NotificationTypeHangout,
+ NotificationTypeGMail,
+ NotificationTypeWeather,
+ NotificationTypeMusic,
+ NotificationTypeMissedCall,
+ NotificationTypeAlarm,
+ NotificationTypeReminder,
+ };
+
+ Notification(const QString &sourceId = QString());
+
+ QString sourceId() const;
+ void setSourceId(const QString &sourceId);
+
+ QString sourceName() const;
+ void setSourceName(const QString &sourceName);
+
+ QString sender() const;
+ void setSender(const QString &sender);
+
+ QString subject() const;
+ void setSubject(const QString &subject);
+
+ QString body() const;
+ void setBody(const QString &body);
+
+ NotificationType type() const;
+ void setType(NotificationType type);
+
+ QString actToken() const;
+ void setActToken(QString actToken);
+
+private:
+ QString m_sourceId;
+ QString m_sourceName;
+ QString m_sender;
+ QString m_subject;
+ QString m_body;
+ NotificationType m_type = NotificationTypeGeneric;
+ QString m_actToken;
+};
+
+#endif // NOTIFICATION_H
diff --git a/rockworkd/libpebble/notificationendpoint.cpp b/rockworkd/libpebble/notificationendpoint.cpp
new file mode 100644
index 0000000..363563a
--- /dev/null
+++ b/rockworkd/libpebble/notificationendpoint.cpp
@@ -0,0 +1,46 @@
+#include "notificationendpoint.h"
+
+#include "watchconnection.h"
+#include "pebble.h"
+#include "blobdb.h"
+
+#include <QDebug>
+#include <QDateTime>
+
+NotificationEndpoint::NotificationEndpoint(Pebble *pebble, WatchConnection *watchConnection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_watchConnection(watchConnection)
+{
+}
+
+void NotificationEndpoint::sendLegacyNotification(const Notification &notification)
+{
+ LegacyNotification::Source source = LegacyNotification::SourceSMS;
+ switch (notification.type()) {
+ case Notification::NotificationTypeEmail:
+ source = LegacyNotification::SourceEmail;
+ break;
+ case Notification::NotificationTypeFacebook:
+ source = LegacyNotification::SourceFacebook;
+ break;
+ case Notification::NotificationTypeSMS:
+ source = LegacyNotification::SourceSMS;
+ break;
+ case Notification::NotificationTypeTwitter:
+ source = LegacyNotification::SourceTwitter;
+ break;
+ default:
+ source = LegacyNotification::SourceSMS;
+ }
+
+ QString body = notification.subject().isEmpty() ? notification.body() : notification.subject();
+ LegacyNotification legacyNotification(source, notification.sender(), body, QDateTime::currentDateTime(), notification.subject());
+ m_watchConnection->writeToPebble(WatchConnection::EndpointNotification, legacyNotification.serialize());
+}
+
+void NotificationEndpoint::notificationReply(const QByteArray &data)
+{
+ qDebug() << "have notification reply" << data.toHex();
+
+}
diff --git a/rockworkd/libpebble/notificationendpoint.h b/rockworkd/libpebble/notificationendpoint.h
new file mode 100644
index 0000000..211c8cd
--- /dev/null
+++ b/rockworkd/libpebble/notificationendpoint.h
@@ -0,0 +1,64 @@
+#include <QObject>
+#include <QUuid>
+#include <QDateTime>
+
+#include "pebble.h"
+#include "watchconnection.h"
+
+class LegacyNotification: public PebblePacket
+{
+// class Meta:
+// endpoint = 3000
+// endianness = '<'
+public:
+ enum Source {
+ SourceEmail = 0,
+ SourceSMS = 1,
+ SourceFacebook = 2,
+ SourceTwitter = 3
+ };
+
+ LegacyNotification(Source source, const QString &sender, const QString &body, const QDateTime &timestamp, const QString &subject):
+ PebblePacket(),
+ m_source(source),
+ m_sender(sender),
+ m_body(body),
+ m_timestamp(timestamp),
+ m_subject(subject)
+ {}
+
+ QByteArray serialize() const override
+ {
+ QByteArray ret;
+ ret.append((quint8)m_source);
+ ret.append(packString(m_sender));
+ ret.append(packString(m_body));
+ ret.append(packString(QString::number(m_timestamp.toMSecsSinceEpoch())));
+ ret.append(packString(m_subject));
+ return ret;
+ }
+
+private:
+
+ Source m_source; // uint8
+ QString m_sender;
+ QString m_body;
+ QDateTime m_timestamp;
+ QString m_subject;
+};
+
+class NotificationEndpoint: public QObject
+{
+ Q_OBJECT
+public:
+ NotificationEndpoint(Pebble *pebble, WatchConnection *watchConnection);
+
+ void sendLegacyNotification(const Notification &notification);
+
+private slots:
+ void notificationReply(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_watchConnection;
+};
diff --git a/rockworkd/libpebble/pebble.cpp b/rockworkd/libpebble/pebble.cpp
new file mode 100644
index 0000000..5655cc7
--- /dev/null
+++ b/rockworkd/libpebble/pebble.cpp
@@ -0,0 +1,693 @@
+#include "pebble.h"
+#include "watchconnection.h"
+#include "notificationendpoint.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "musicendpoint.h"
+#include "phonecallendpoint.h"
+#include "appmanager.h"
+#include "appmsgmanager.h"
+#include "jskit/jskitmanager.h"
+#include "blobdb.h"
+#include "appdownloader.h"
+#include "screenshotendpoint.h"
+#include "firmwaredownloader.h"
+#include "watchlogendpoint.h"
+#include "core.h"
+#include "platforminterface.h"
+#include "ziphelper.h"
+#include "dataloggingendpoint.h"
+
+#include "QDir"
+#include <QDateTime>
+#include <QStandardPaths>
+#include <QSettings>
+#include <QTimeZone>
+
+Pebble::Pebble(const QBluetoothAddress &address, QObject *parent):
+ QObject(parent),
+ m_address(address)
+{
+ m_storagePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + m_address.toString().replace(':', '_') + "/";
+
+ m_connection = new WatchConnection(this);
+ QObject::connect(m_connection, &WatchConnection::watchConnected, this, &Pebble::onPebbleConnected);
+ QObject::connect(m_connection, &WatchConnection::watchDisconnected, this, &Pebble::onPebbleDisconnected);
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointVersion, this, "pebbleVersionReceived");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointPhoneVersion, this, "phoneVersionAsked");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointFactorySettings, this, "factorySettingsReceived");
+
+ m_dataLogEndpoint = new DataLoggingEndpoint(this, m_connection);
+
+ m_notificationEndpoint = new NotificationEndpoint(this, m_connection);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::notificationReceived, this, &Pebble::sendNotification);
+
+ m_musicEndpoint = new MusicEndpoint(this, m_connection);
+ m_musicEndpoint->setMusicMetadata(Core::instance()->platform()->musicMetaData());
+ QObject::connect(m_musicEndpoint, &MusicEndpoint::musicControlPressed, Core::instance()->platform(), &PlatformInterface::sendMusicControlCommand);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::musicMetadataChanged, m_musicEndpoint, &MusicEndpoint::setMusicMetadata);
+
+ m_phoneCallEndpoint = new PhoneCallEndpoint(this, m_connection);
+ QObject::connect(m_phoneCallEndpoint, &PhoneCallEndpoint::hangupCall, Core::instance()->platform(), &PlatformInterface::hangupCall);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::incomingCall, m_phoneCallEndpoint, &PhoneCallEndpoint::incomingCall);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::callStarted, m_phoneCallEndpoint, &PhoneCallEndpoint::callStarted);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::callEnded, m_phoneCallEndpoint, &PhoneCallEndpoint::callEnded);
+
+ m_appManager = new AppManager(this, m_connection);
+ QObject::connect(m_appManager, &AppManager::appsChanged, this, &Pebble::installedAppsChanged);
+ QObject::connect(m_appManager, &AppManager::idMismatchDetected, this, &Pebble::resetPebble);
+
+ m_appMsgManager = new AppMsgManager(this, m_appManager, m_connection);
+ m_jskitManager = new JSKitManager(this, m_connection, m_appManager, m_appMsgManager, this);
+ QObject::connect(m_jskitManager, SIGNAL(openURL(const QString&, const QString&)), this, SIGNAL(openURL(const QString&, const QString&)));
+
+ m_blobDB = new BlobDB(this, m_connection);
+ QObject::connect(m_blobDB, &BlobDB::muteSource, this, &Pebble::muteNotificationSource);
+ QObject::connect(m_blobDB, &BlobDB::actionTriggered, Core::instance()->platform(), &PlatformInterface::actionTriggered);
+ QObject::connect(m_blobDB, &BlobDB::appInserted, this, &Pebble::appInstalled);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::organizerItemsChanged, this, &Pebble::syncCalendar);
+
+ m_appDownloader = new AppDownloader(m_storagePath, this);
+ QObject::connect(m_appDownloader, &AppDownloader::downloadFinished, this, &Pebble::appDownloadFinished);
+
+ m_screenshotEndpoint = new ScreenshotEndpoint(this, m_connection, this);
+ QObject::connect(m_screenshotEndpoint, &ScreenshotEndpoint::screenshotAdded, this, &Pebble::screenshotAdded);
+ QObject::connect(m_screenshotEndpoint, &ScreenshotEndpoint::screenshotRemoved, this, &Pebble::screenshotRemoved);
+
+ m_firmwareDownloader = new FirmwareDownloader(this, m_connection);
+ QObject::connect(m_firmwareDownloader, &FirmwareDownloader::updateAvailableChanged, this, &Pebble::slotUpdateAvailableChanged);
+ QObject::connect(m_firmwareDownloader, &FirmwareDownloader::upgradingChanged, this, &Pebble::upgradingFirmwareChanged);
+
+ m_logEndpoint = new WatchLogEndpoint(this, m_connection);
+ QObject::connect(m_logEndpoint, &WatchLogEndpoint::logsFetched, this, &Pebble::logsDumped);
+
+ QSettings watchInfo(m_storagePath + "/watchinfo.conf", QSettings::IniFormat);
+ m_model = (Model)watchInfo.value("watchModel", (int)ModelUnknown).toInt();
+
+ QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ settings.beginGroup("activityParams");
+ m_healthParams.setEnabled(settings.value("enabled").toBool());
+ m_healthParams.setAge(settings.value("age").toUInt());
+ m_healthParams.setHeight(settings.value("height").toInt());
+ m_healthParams.setGender((HealthParams::Gender)settings.value("gender").toInt());
+ m_healthParams.setWeight(settings.value("weight").toInt());
+ m_healthParams.setMoreActive(settings.value("moreActive").toBool());
+ m_healthParams.setSleepMore(settings.value("sleepMore").toBool());
+ settings.endGroup();
+
+ settings.beginGroup("unitsDistance");
+ m_imperialUnits = settings.value("imperialUnits", false).toBool();
+ settings.endGroup();
+
+ settings.beginGroup("calendar");
+ m_calendarSyncEnabled = settings.value("calendarSyncEnabled", true).toBool();
+ settings.endGroup();
+}
+
+QBluetoothAddress Pebble::address() const
+{
+ return m_address;
+}
+
+QString Pebble::name() const
+{
+ return m_name;
+}
+
+void Pebble::setName(const QString &name)
+{
+ m_name = name;
+}
+
+QBluetoothLocalDevice::Pairing Pebble::pairingStatus() const
+{
+ QBluetoothLocalDevice dev;
+ return dev.pairingStatus(m_address);
+}
+
+bool Pebble::connected() const
+{
+ return m_connection->isConnected() && !m_serialNumber.isEmpty();
+}
+
+void Pebble::connect()
+{
+ qDebug() << "Connecting to Pebble:" << m_name << m_address;
+ m_connection->connectPebble(m_address);
+}
+
+QDateTime Pebble::softwareBuildTime() const
+{
+ return m_softwareBuildTime;
+}
+
+QString Pebble::softwareVersion() const
+{
+ return m_softwareVersion;
+}
+
+QString Pebble::softwareCommitRevision() const
+{
+ return m_softwareCommitRevision;
+}
+
+HardwareRevision Pebble::hardwareRevision() const
+{
+ return m_hardwareRevision;
+}
+
+Model Pebble::model() const
+{
+ return m_model;
+}
+
+void Pebble::setHardwareRevision(HardwareRevision hardwareRevision)
+{
+ m_hardwareRevision = hardwareRevision;
+ switch (m_hardwareRevision) {
+ case HardwareRevisionUNKNOWN:
+ m_hardwarePlatform = HardwarePlatformUnknown;
+ break;
+ case HardwareRevisionTINTIN_EV1:
+ case HardwareRevisionTINTIN_EV2:
+ case HardwareRevisionTINTIN_EV2_3:
+ case HardwareRevisionTINTIN_EV2_4:
+ case HardwareRevisionTINTIN_V1_5:
+ case HardwareRevisionBIANCA:
+ case HardwareRevisionTINTIN_BB:
+ case HardwareRevisionTINTIN_BB2:
+ m_hardwarePlatform = HardwarePlatformAplite;
+ break;
+ case HardwareRevisionSNOWY_EVT2:
+ case HardwareRevisionSNOWY_DVT:
+ case HardwareRevisionBOBBY_SMILES:
+ case HardwareRevisionSNOWY_BB:
+ case HardwareRevisionSNOWY_BB2:
+ m_hardwarePlatform = HardwarePlatformBasalt;
+ break;
+ case HardwareRevisionSPALDING_EVT:
+ case HardwareRevisionSPALDING:
+ case HardwareRevisionSPALDING_BB2:
+ m_hardwarePlatform = HardwarePlatformChalk;
+ break;
+ }
+}
+
+HardwarePlatform Pebble::hardwarePlatform() const
+{
+ return m_hardwarePlatform;
+}
+
+QString Pebble::serialNumber() const
+{
+ return m_serialNumber;
+}
+
+QString Pebble::language() const
+{
+ return m_language;
+}
+
+Capabilities Pebble::capabilities() const
+{
+ return m_capabilities;
+}
+
+bool Pebble::isUnfaithful() const
+{
+ return m_isUnfaithful;
+}
+
+bool Pebble::recovery() const
+{
+ return m_recovery;
+}
+
+bool Pebble::upgradingFirmware() const
+{
+ return m_firmwareDownloader->upgrading();
+}
+
+void Pebble::setHealthParams(const HealthParams &healthParams)
+{
+ m_healthParams = healthParams;
+ m_blobDB->setHealthParams(healthParams);
+ emit healtParamsChanged();
+
+ QSettings healthSettings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ healthSettings.beginGroup("activityParams");
+ healthSettings.setValue("enabled", m_healthParams.enabled());
+ healthSettings.setValue("age", m_healthParams.age());
+ healthSettings.setValue("height", m_healthParams.height());
+ healthSettings.setValue("gender", m_healthParams.gender());
+ healthSettings.setValue("weight", m_healthParams.weight());
+ healthSettings.setValue("moreActive", m_healthParams.moreActive());
+ healthSettings.setValue("sleepMore", m_healthParams.sleepMore());
+
+}
+
+HealthParams Pebble::healthParams() const
+{
+ return m_healthParams;
+}
+
+void Pebble::setImperialUnits(bool imperial)
+{
+ m_imperialUnits = imperial;
+ m_blobDB->setUnits(imperial);
+ emit imperialUnitsChanged();
+
+ QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ settings.beginGroup("unitsDistance");
+ settings.setValue("enabled", m_imperialUnits);
+}
+
+bool Pebble::imperialUnits() const
+{
+ return m_imperialUnits;
+}
+
+void Pebble::dumpLogs(const QString &fileName) const
+{
+ m_logEndpoint->fetchLogs(fileName);
+}
+
+QString Pebble::storagePath() const
+{
+ return m_storagePath;
+}
+
+QHash<QString, bool> Pebble::notificationsFilter() const
+{
+ QHash<QString, bool> ret;
+ QString settingsFile = m_storagePath + "/notifications.conf";
+ QSettings s(settingsFile, QSettings::IniFormat);
+ foreach (const QString &key, s.allKeys()) {
+ ret.insert(key, s.value(key).toBool());
+ }
+ return ret;
+}
+
+void Pebble::setNotificationFilter(const QString &sourceId, bool enabled)
+{
+ QString settingsFile = m_storagePath + "/notifications.conf";
+ QSettings s(settingsFile, QSettings::IniFormat);
+ if (!s.contains(sourceId) || s.value(sourceId).toBool() != enabled) {
+ s.setValue(sourceId, enabled);
+ emit notificationFilterChanged(sourceId, enabled);
+ }
+}
+
+void Pebble::sendNotification(const Notification &notification)
+{
+ if (!notificationsFilter().value(notification.sourceId(), true)) {
+ qDebug() << "Notifications for" << notification.sourceId() << "disabled.";
+ return;
+ }
+ // In case it wasn't there before, make sure to write it to the config now so it will appear in the config app.
+ setNotificationFilter(notification.sourceId(), true);
+
+ qDebug() << "Sending notification from source" << notification.sourceId() << "to watch";
+
+ if (m_softwareVersion < "v3.0") {
+ m_notificationEndpoint->sendLegacyNotification(notification);
+ } else {
+ m_blobDB->insertNotification(notification);
+ }
+}
+
+void Pebble::clearAppDB()
+{
+ m_blobDB->clearApps();
+}
+
+void Pebble::clearTimeline()
+{
+ m_blobDB->clearTimeline();
+}
+
+void Pebble::setCalendarSyncEnabled(bool enabled)
+{
+ if (m_calendarSyncEnabled == enabled) {
+ return;
+ }
+ m_calendarSyncEnabled = enabled;
+ emit calendarSyncEnabledChanged();
+
+ if (!m_calendarSyncEnabled) {
+ m_blobDB->clearTimeline();
+ } else {
+ syncCalendar(Core::instance()->platform()->organizerItems());
+ }
+
+ QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ settings.beginGroup("calendar");
+ settings.setValue("calendarSyncEnabled", m_calendarSyncEnabled);
+ settings.endGroup();
+}
+
+bool Pebble::calendarSyncEnabled() const
+{
+ return m_calendarSyncEnabled;
+}
+
+void Pebble::syncCalendar(const QList<CalendarEvent> &items)
+{
+ if (connected() && m_calendarSyncEnabled) {
+ m_blobDB->syncCalendar(items);
+ }
+}
+
+void Pebble::installApp(const QString &id)
+{
+ m_appDownloader->downloadApp(id);
+}
+
+void Pebble::sideloadApp(const QString &packageFile)
+{
+ QString targetFile = packageFile;
+ targetFile.remove("file://");
+
+ QString id;
+ int i = 0;
+ do {
+ QDir dir(m_storagePath + "/apps/sideload" + QString::number(i));
+ if (!dir.exists()) {
+ if (!dir.mkpath(dir.absolutePath())) {
+ qWarning() << "Error creating dir for unpacking. Cannot install package" << packageFile;
+ return;
+ }
+ id = "sideload" + QString::number(i);
+ }
+ i++;
+ } while (id.isEmpty());
+
+ if (!ZipHelper::unpackArchive(targetFile, m_storagePath + "/apps/" + id)) {
+ qWarning() << "Error unpacking App zip file" << targetFile << "to" << m_storagePath + "/apps/" + id;
+ return;
+ }
+
+ qDebug() << "Sideload package unpacked.";
+ appDownloadFinished(id);
+}
+
+QList<QUuid> Pebble::installedAppIds()
+{
+ return m_appManager->appUuids();
+}
+
+void Pebble::setAppOrder(const QList<QUuid> &newList)
+{
+ m_appManager->setAppOrder(newList);
+}
+
+AppInfo Pebble::appInfo(const QUuid &uuid)
+{
+ return m_appManager->info(uuid);
+}
+
+void Pebble::removeApp(const QUuid &uuid)
+{
+ qDebug() << "Should remove app:" << uuid;
+ m_blobDB->removeApp(m_appManager->info(uuid));
+ m_appManager->removeApp(uuid);
+}
+
+void Pebble::launchApp(const QUuid &uuid)
+{
+ m_appMsgManager->launchApp(uuid);
+}
+
+void Pebble::requestConfigurationURL(const QUuid &uuid) {
+ if (m_jskitManager->currentApp().uuid() == uuid) {
+ m_jskitManager->showConfiguration();
+ }
+ else {
+ m_jskitManager->setConfigurationId(uuid);
+ m_appMsgManager->launchApp(uuid);
+ }
+}
+
+void Pebble::configurationClosed(const QUuid &uuid, const QString &result)
+{
+ if (m_jskitManager->currentApp().uuid() == uuid) {
+ m_jskitManager->handleWebviewClosed(result);
+ }
+}
+
+void Pebble::requestScreenshot()
+{
+ m_screenshotEndpoint->requestScreenshot();
+}
+
+QStringList Pebble::screenshots() const
+{
+ return m_screenshotEndpoint->screenshots();
+}
+
+void Pebble::removeScreenshot(const QString &filename)
+{
+ m_screenshotEndpoint->removeScreenshot(filename);
+}
+
+bool Pebble::firmwareUpdateAvailable() const
+{
+ return m_firmwareDownloader->updateAvailable();
+}
+
+QString Pebble::candidateFirmwareVersion() const
+{
+ return m_firmwareDownloader->candidateVersion();
+}
+
+QString Pebble::firmwareReleaseNotes() const
+{
+ return m_firmwareDownloader->releaseNotes();
+}
+
+void Pebble::upgradeFirmware() const
+{
+ m_firmwareDownloader->performUpgrade();
+}
+
+void Pebble::onPebbleConnected()
+{
+ qDebug() << "Pebble connected:" << m_name;
+ QByteArray data;
+ WatchDataWriter w(&data);
+ w.write<quint8>(0); // Command fetch
+ QString message = "mfg_color";
+ w.writeLE<quint8>(message.length());
+ w.writeFixedString(message.length(), message);
+ m_connection->writeToPebble(WatchConnection::EndpointFactorySettings, data);
+
+ m_connection->writeToPebble(WatchConnection::EndpointVersion, QByteArray(1, 0));
+}
+
+void Pebble::onPebbleDisconnected()
+{
+ qDebug() << "Pebble disconnected:" << m_name;
+ emit pebbleDisconnected();
+}
+
+void Pebble::pebbleVersionReceived(const QByteArray &data)
+{
+ WatchDataReader wd(data);
+
+ wd.skip(1);
+ m_softwareBuildTime = QDateTime::fromTime_t(wd.read<quint32>());
+ qDebug() << "Software Version build:" << m_softwareBuildTime;
+ m_softwareVersion = wd.readFixedString(32);
+ qDebug() << "Software Version string:" << m_softwareVersion;
+ m_softwareCommitRevision = wd.readFixedString(8);
+ qDebug() << "Software Version commit:" << m_softwareCommitRevision;
+
+ m_recovery = wd.read<quint8>();
+ qDebug() << "Recovery:" << m_recovery;
+ HardwareRevision rev = (HardwareRevision)wd.read<quint8>();
+ setHardwareRevision(rev);
+ qDebug() << "HW Revision:" << rev;
+ qDebug() << "Metadata Version:" << wd.read<quint8>();
+
+ qDebug() << "Safe build:" << QDateTime::fromTime_t(wd.read<quint32>());
+ qDebug() << "Safe version:" << wd.readFixedString(32);
+ qDebug() << "safe commit:" << wd.readFixedString(8);
+ qDebug() << "Safe recovery:" << wd.read<quint8>();
+ qDebug() << "HW Revision:" << wd.read<quint8>();
+ qDebug() << "Metadata Version:" << wd.read<quint8>();
+
+ qDebug() << "BootloaderBuild" << QDateTime::fromTime_t(wd.read<quint32>());
+ qDebug() << "hardwareRevision" << wd.readFixedString(9);
+ m_serialNumber = wd.readFixedString(12);
+ qDebug() << "serialnumber" << m_serialNumber;
+ qDebug() << "BT address" << wd.readBytes(6).toHex();
+ qDebug() << "CRC:" << wd.read<quint32>();
+ qDebug() << "Resource timestamp:" << QDateTime::fromTime_t(wd.read<quint32>());
+ m_language = wd.readFixedString(6);
+ qDebug() << "Language" << m_language;
+ qDebug() << "Language version" << wd.read<quint16>();
+ // Capabilities is 64 bits but QFlags can only do 32 bits. lets split it into 2 * 32.
+ // only 8 bits are used atm anyways.
+ m_capabilities = QFlag(wd.readLE<quint32>());
+ qDebug() << "Capabilities" << QString::number(m_capabilities, 16);
+ qDebug() << "Capabilities" << wd.readLE<quint32>();
+ m_isUnfaithful = wd.read<quint8>();
+ qDebug() << "Is Unfaithful" << m_isUnfaithful;
+
+ // This is useful for debugging
+ //m_isUnfaithful = true;
+
+ if (!m_recovery) {
+ m_appManager->rescan();
+
+ QSettings version(m_storagePath + "/watchinfo.conf", QSettings::IniFormat);
+ if (version.value("syncedWithVersion").toString() != QStringLiteral(VERSION)) {
+ m_isUnfaithful = true;
+ }
+
+ if (m_isUnfaithful) {
+ qDebug() << "Pebble sync state unclear. Resetting Pebble watch.";
+ resetPebble();
+ } else {
+ syncCalendar(Core::instance()->platform()->organizerItems());
+ syncApps();
+ m_blobDB->setHealthParams(m_healthParams);
+ m_blobDB->setUnits(m_imperialUnits);
+ }
+ version.setValue("syncedWithVersion", QStringLiteral(VERSION));
+
+ syncTime();
+ }
+
+ m_firmwareDownloader->checkForNewFirmware();
+ emit pebbleConnected();
+
+}
+
+void Pebble::factorySettingsReceived(const QByteArray &data)
+{
+ qDebug() << "have factory settings" << data.toHex();
+
+ WatchDataReader reader(data);
+ quint8 status = reader.read<quint8>();
+ quint8 len = reader.read<quint8>();
+
+ if (status != 0x01 && len != 0x04) {
+ qWarning() << "Unexpected data reading factory settings";
+ return;
+ }
+ m_model = (Model)reader.read<quint32>();
+ QSettings s(m_storagePath + "/watchinfo.conf", QSettings::IniFormat);
+ s.setValue("watchModel", m_model);
+}
+
+void Pebble::phoneVersionAsked(const QByteArray &data)
+{
+
+ QByteArray res;
+
+ Capabilities sessionCap(CapabilityHealth
+ | CapabilityAppRunState
+ | CapabilityUpdatedMusicProtocol | CapabilityInfiniteLogDumping | Capability8kAppMessages);
+
+ quint32 platformFlags = 16 | 32 | OSAndroid;
+
+ WatchDataWriter writer(&res);
+ writer.writeLE<quint8>(0x01); // ok
+ writer.writeLE<quint32>(0xFFFFFFFF);
+ writer.writeLE<quint32>(sessionCap);
+ writer.write<quint32>(platformFlags);
+ writer.write<quint8>(2); // response version
+ writer.write<quint8>(3); // major version
+ writer.write<quint8>(0); // minor version
+ writer.write<quint8>(0); // bugfix version
+ writer.writeLE<quint64>(sessionCap);
+
+ qDebug() << "sending phone version" << res.toHex();
+
+ m_connection->writeToPebble(WatchConnection::EndpointPhoneVersion, res);
+}
+
+void Pebble::appDownloadFinished(const QString &id)
+{
+ QUuid uuid = m_appManager->scanApp(m_storagePath + "/apps/" + id);
+ if (uuid.isNull()) {
+ qWarning() << "Error scanning downloaded app. Won't install on watch";
+ return;
+ }
+ m_blobDB->insertAppMetaData(m_appManager->info(uuid));
+ m_pendingInstallations.append(uuid);
+}
+
+void Pebble::appInstalled(const QUuid &uuid) {
+ if (m_pendingInstallations.contains(uuid)) {
+ m_appMsgManager->launchApp(uuid);
+ }
+}
+
+void Pebble::muteNotificationSource(const QString &source)
+{
+ setNotificationFilter(source, false);
+}
+
+void Pebble::resetPebble()
+{
+ clearTimeline();
+ syncCalendar(Core::instance()->platform()->organizerItems());
+
+ clearAppDB();
+ syncApps();
+}
+
+void Pebble::syncApps()
+{
+ foreach (const QUuid &appUuid, m_appManager->appUuids()) {
+ if (!m_appManager->info(appUuid).isSystemApp()) {
+ qDebug() << "Inserting app" << m_appManager->info(appUuid).shortName() << "into BlobDB";
+ m_blobDB->insertAppMetaData(m_appManager->info(appUuid));
+ }
+ }
+ // make sure the order is synced too
+ m_appManager->setAppOrder(m_appManager->appUuids());
+}
+
+void Pebble::syncTime()
+{
+ TimeMessage msg(TimeMessage::TimeOperationSetUTC);
+ qDebug() << "Syncing Time" << QDateTime::currentDateTime() << msg.serialize().toHex();
+ m_connection->writeToPebble(WatchConnection::EndpointTime, msg.serialize());
+}
+
+void Pebble::slotUpdateAvailableChanged()
+{
+ qDebug() << "update available" << m_firmwareDownloader->updateAvailable() << m_firmwareDownloader->candidateVersion();
+
+ emit updateAvailableChanged();
+}
+
+
+TimeMessage::TimeMessage(TimeMessage::TimeOperation operation) :
+ m_operation(operation)
+{
+
+}
+QByteArray TimeMessage::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.write<quint8>(m_operation);
+ switch (m_operation) {
+ case TimeOperationSetLocaltime:
+ writer.writeLE<quint32>(QDateTime::currentMSecsSinceEpoch() / 1000);
+ break;
+ case TimeOperationSetUTC:
+ writer.write<quint32>(QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000);
+ writer.write<qint16>(QDateTime::currentDateTime().offsetFromUtc() / 60);
+ writer.writePascalString(QDateTime::currentDateTime().timeZone().displayName(QTimeZone::StandardTime));
+ break;
+ default:
+ ;
+ }
+ return ret;
+}
diff --git a/rockworkd/libpebble/pebble.h b/rockworkd/libpebble/pebble.h
new file mode 100644
index 0000000..8650d74
--- /dev/null
+++ b/rockworkd/libpebble/pebble.h
@@ -0,0 +1,225 @@
+#ifndef PEBBLE_H
+#define PEBBLE_H
+
+#include "musicmetadata.h"
+#include "notification.h"
+#include "calendarevent.h"
+#include "appinfo.h"
+#include "healthparams.h"
+
+#include <QObject>
+#include <QBluetoothAddress>
+#include <QBluetoothLocalDevice>
+#include <QTimer>
+
+class WatchConnection;
+class NotificationEndpoint;
+class MusicEndpoint;
+class PhoneCallEndpoint;
+class AppManager;
+class AppMsgManager;
+class BankManager;
+class JSKitManager;
+class BlobDB;
+class AppDownloader;
+class ScreenshotEndpoint;
+class FirmwareDownloader;
+class WatchLogEndpoint;
+class DataLoggingEndpoint;
+
+class Pebble : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(Pebble::NotificationType)
+ Q_PROPERTY(QBluetoothAddress address MEMBER m_address)
+ Q_PROPERTY(QString name MEMBER m_name)
+ Q_PROPERTY(HardwareRevision HardwareRevision READ hardwareRevision)
+ Q_PROPERTY(Model model READ model)
+ Q_PROPERTY(HardwarePlatform hardwarePlatform MEMBER m_hardwarePlatform)
+ Q_PROPERTY(QString softwareVersion MEMBER m_softwareVersion)
+ Q_PROPERTY(QString serialNumber MEMBER m_serialNumber)
+ Q_PROPERTY(QString language MEMBER m_language)
+
+public:
+ explicit Pebble(const QBluetoothAddress &address, QObject *parent = 0);
+
+ QBluetoothAddress address() const;
+
+ QString name() const;
+ void setName(const QString &name);
+
+ QBluetoothLocalDevice::Pairing pairingStatus() const;
+
+ bool connected() const;
+ void connect();
+
+ QDateTime softwareBuildTime() const;
+ QString softwareVersion() const;
+ QString softwareCommitRevision() const;
+ HardwareRevision hardwareRevision() const;
+ Model model() const;
+ HardwarePlatform hardwarePlatform() const;
+ QString serialNumber() const;
+ QString language() const;
+ Capabilities capabilities() const;
+ bool isUnfaithful() const;
+ bool recovery() const;
+
+ QString storagePath() const;
+
+public slots:
+ QHash<QString, bool> notificationsFilter() const;
+ void setNotificationFilter(const QString &sourceId, bool enabled);
+ void sendNotification(const Notification &notification);
+
+ void clearTimeline();
+ void setCalendarSyncEnabled(bool enabled);
+ bool calendarSyncEnabled() const;
+
+ void clearAppDB();
+ void installApp(const QString &id);
+ void sideloadApp(const QString &packageFile);
+ QList<QUuid> installedAppIds();
+ void setAppOrder(const QList<QUuid> &newList);
+ AppInfo appInfo(const QUuid &uuid);
+ void removeApp(const QUuid &uuid);
+
+ void launchApp(const QUuid &uuid);
+
+ void requestConfigurationURL(const QUuid &uuid);
+ void configurationClosed(const QUuid &uuid, const QString &result);
+
+ void requestScreenshot();
+ QStringList screenshots() const;
+ void removeScreenshot(const QString &filename);
+
+ bool firmwareUpdateAvailable() const;
+ QString candidateFirmwareVersion() const;
+ QString firmwareReleaseNotes() const;
+ void upgradeFirmware() const;
+ bool upgradingFirmware() const;
+
+ void setHealthParams(const HealthParams &healthParams);
+ HealthParams healthParams() const;
+
+ void setImperialUnits(bool imperial);
+ bool imperialUnits() const;
+
+ void dumpLogs(const QString &fileName) const;
+
+private slots:
+ void onPebbleConnected();
+ void onPebbleDisconnected();
+ void pebbleVersionReceived(const QByteArray &data);
+ void factorySettingsReceived(const QByteArray &data);
+ void phoneVersionAsked(const QByteArray &data);
+ void appDownloadFinished(const QString &id);
+ void appInstalled(const QUuid &uuid);
+ void muteNotificationSource(const QString &source);
+
+ void resetPebble();
+ void syncApps();
+ void syncTime();
+ void syncCalendar(const QList<CalendarEvent> &items);
+
+ void slotUpdateAvailableChanged();
+
+signals:
+ void pebbleConnected();
+ void pebbleDisconnected();
+ void notificationFilterChanged(const QString &sourceId, bool enabled);
+ void musicControlPressed(MusicControlButton control);
+ void installedAppsChanged();
+ void openURL(const QString &uuid, const QString &url);
+ void screenshotAdded(const QString &filename);
+ void screenshotRemoved(const QString &filename);
+ void updateAvailableChanged();
+ void upgradingFirmwareChanged();
+ void logsDumped(bool success);
+
+ void calendarSyncEnabledChanged();
+ void imperialUnitsChanged();
+ void healtParamsChanged();
+private:
+ void setHardwareRevision(HardwareRevision hardwareRevision);
+
+ QBluetoothAddress m_address;
+ QString m_name;
+ QDateTime m_softwareBuildTime;
+ QString m_softwareVersion;
+ QString m_softwareCommitRevision;
+ HardwareRevision m_hardwareRevision;
+ HardwarePlatform m_hardwarePlatform = HardwarePlatformUnknown;
+ Model m_model = ModelUnknown;
+ QString m_serialNumber;
+ QString m_language;
+ Capabilities m_capabilities = CapabilityNone;
+ bool m_isUnfaithful = false;
+ bool m_recovery = false;
+
+ WatchConnection *m_connection;
+ NotificationEndpoint *m_notificationEndpoint;
+ MusicEndpoint *m_musicEndpoint;
+ PhoneCallEndpoint *m_phoneCallEndpoint;
+ AppManager *m_appManager;
+ AppMsgManager *m_appMsgManager;
+ JSKitManager *m_jskitManager;
+ BankManager *m_bankManager;
+ BlobDB *m_blobDB;
+ AppDownloader *m_appDownloader;
+ ScreenshotEndpoint *m_screenshotEndpoint;
+ FirmwareDownloader *m_firmwareDownloader;
+ WatchLogEndpoint *m_logEndpoint;
+ DataLoggingEndpoint *m_dataLogEndpoint;
+
+ QString m_storagePath;
+ QList<QUuid> m_pendingInstallations;
+
+ bool m_calendarSyncEnabled = true;
+ HealthParams m_healthParams;
+ bool m_imperialUnits = false;
+};
+
+/*
+ Capabilities received from phone:
+ In order, starting at zero, in little-endian (unlike the rest of the messsage), the bits sent by the watch indicate support for:
+ - app run state,
+ - infinite log dumping,
+ - updated music protocol,
+ - extended notification service,
+ - language packs,
+ - 8k app messages,
+ - health,
+ - voice
+
+ The capability bits sent *to* the watch are, starting at zero:
+ - app run state,
+ - infinite log dumping,
+ - updated music service,
+ - extended notification service,
+ - (unused),
+ - 8k app messages,
+ - (unused),
+ - third-party voice
+ */
+
+
+
+class TimeMessage: public PebblePacket
+{
+public:
+ enum TimeOperation {
+ TimeOperationGetRequest = 0x00,
+ TimeOperationGetResponse = 0x01,
+ TimeOperationSetLocaltime = 0x02,
+ TimeOperationSetUTC = 0x03
+ };
+ TimeMessage(TimeOperation operation);
+
+ QByteArray serialize() const override;
+
+private:
+ TimeOperation m_operation = TimeOperationGetRequest;
+};
+
+#endif // PEBBLE_H
diff --git a/rockworkd/libpebble/phonecallendpoint.cpp b/rockworkd/libpebble/phonecallendpoint.cpp
new file mode 100644
index 0000000..afd869d
--- /dev/null
+++ b/rockworkd/libpebble/phonecallendpoint.cpp
@@ -0,0 +1,71 @@
+#include "phonecallendpoint.h"
+
+#include "pebble.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+
+PhoneCallEndpoint::PhoneCallEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointPhoneControl, this, "handlePhoneEvent");
+}
+
+void PhoneCallEndpoint::incomingCall(uint cookie, const QString &number, const QString &name)
+{
+ QStringList tmp;
+ tmp.append(number);
+ tmp.append(name);
+
+ char act = CallActionIncoming;
+ // FIXME: Outgoing calls don't seem to work... Maybe something wrong in the enum?
+// if (!incoming) {
+// act = CallActionOutgoing;
+// }
+
+ phoneControl(act, cookie, tmp);
+
+}
+
+void PhoneCallEndpoint::callStarted(uint cookie)
+{
+ phoneControl(CallActionStart, cookie, QStringList());
+}
+
+void PhoneCallEndpoint::callEnded(uint cookie, bool missed)
+{
+ Q_UNUSED(missed)
+ // FIXME: The watch doesn't seem to react on Missed... So let's always "End" it for now
+// phoneControl(missed ? CallActionMissed : CallActionEnd, cookie, QStringList());
+ phoneControl(CallActionEnd, cookie, QStringList());
+}
+
+void PhoneCallEndpoint::phoneControl(char act, uint cookie, QStringList datas)
+{
+ QByteArray head;
+ head.append((char)act);
+ head.append((cookie >> 24)& 0xFF);
+ head.append((cookie >> 16)& 0xFF);
+ head.append((cookie >> 8)& 0xFF);
+ head.append(cookie & 0xFF);
+ if (datas.length()>0) {
+ head.append(m_connection->buildData(datas));
+ }
+
+ m_connection->writeToPebble(WatchConnection::EndpointPhoneControl, head);
+}
+
+void PhoneCallEndpoint::handlePhoneEvent(const QByteArray &data)
+{
+
+ WatchDataReader reader(data);
+ reader.skip(1);
+ uint cookie = reader.read<uint>();
+
+ if (data.at(0) == CallActionHangup) {
+ emit hangupCall(cookie);
+ } else {
+ qWarning() << "received an unhandled phone event" << data.toHex();
+ }
+}
diff --git a/rockworkd/libpebble/phonecallendpoint.h b/rockworkd/libpebble/phonecallendpoint.h
new file mode 100644
index 0000000..994f8a6
--- /dev/null
+++ b/rockworkd/libpebble/phonecallendpoint.h
@@ -0,0 +1,47 @@
+#ifndef PHONECALLENDPOINT_H
+#define PHONECALLENDPOINT_H
+
+#include <QObject>
+
+class Pebble;
+class WatchConnection;
+
+class PhoneCallEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum CallAction{
+ CallActionAnswer = 1,
+ CallActionHangup = 2,
+ CallActionGetState = 3,
+ CallActionIncoming = 4,
+ CallActionOutgoing = 5,
+ CallActionMissed = 6,
+ CallActionRing = 7,
+ CallActionStart = 8,
+ CallActionEnd = 9
+ };
+
+ explicit PhoneCallEndpoint(Pebble *pebble, WatchConnection *connection);
+
+public slots:
+ void incomingCall(uint cookie, const QString &number, const QString &name);
+ void callStarted(uint cookie);
+ void callEnded(uint cookie, bool missed);
+
+signals:
+ void hangupCall(uint cookie);
+
+
+private:
+ void phoneControl(char act, uint cookie, QStringList datas);
+
+private slots:
+ void handlePhoneEvent(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+};
+
+#endif // PHONECALLENDPOINT_H
diff --git a/rockworkd/libpebble/platforminterface.h b/rockworkd/libpebble/platforminterface.h
new file mode 100644
index 0000000..6c67598
--- /dev/null
+++ b/rockworkd/libpebble/platforminterface.h
@@ -0,0 +1,46 @@
+#ifndef PLATFORMINTERFACE_H
+#define PLATFORMINTERFACE_H
+
+#include "libpebble/pebble.h"
+#include "libpebble/musicmetadata.h"
+
+#include <QObject>
+#include <QOrganizerItem>
+
+class PlatformInterface: public QObject
+{
+ Q_OBJECT
+public:
+ PlatformInterface(QObject *parent = 0): QObject(parent) {}
+ virtual ~PlatformInterface() {}
+
+// Notifications
+public:
+ virtual void actionTriggered(const QString &actToken) = 0;
+signals:
+ void notificationReceived(const Notification &notification);
+
+// Music
+public:
+ virtual void sendMusicControlCommand(MusicControlButton controlButton) = 0;
+ virtual MusicMetaData musicMetaData() const = 0;
+signals:
+ void musicMetadataChanged(MusicMetaData metaData);
+
+// Phone calls
+signals:
+ void incomingCall(uint cookie, const QString &number, const QString &name);
+ void callStarted(uint cookie);
+ void callEnded(uint cookie, bool missed);
+public:
+ virtual void hangupCall(uint cookie) = 0;
+
+// Organizer
+public:
+ virtual QList<CalendarEvent> organizerItems() const = 0;
+signals:
+ void organizerItemsChanged(const QList<CalendarEvent> &items);
+
+};
+
+#endif // PLATFORMINTERFACE_H
diff --git a/rockworkd/libpebble/screenshotendpoint.cpp b/rockworkd/libpebble/screenshotendpoint.cpp
new file mode 100644
index 0000000..b31ab70
--- /dev/null
+++ b/rockworkd/libpebble/screenshotendpoint.cpp
@@ -0,0 +1,131 @@
+#include "screenshotendpoint.h"
+
+#include "watchdatawriter.h"
+#include "watchdatareader.h"
+#include "pebble.h"
+
+#include <QImage>
+#include <QDateTime>
+#include <QDir>
+
+ScreenshotEndpoint::ScreenshotEndpoint(Pebble *pebble, WatchConnection *connection, QObject *parent):
+ QObject(parent),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointScreenshot, this, "handleScreenshotData");
+}
+
+void ScreenshotEndpoint::requestScreenshot()
+{
+ ScreenshotRequestPackage package;
+ m_connection->writeToPebble(WatchConnection::EndpointScreenshot, package.serialize());
+}
+
+void ScreenshotEndpoint::removeScreenshot(const QString &filename)
+{
+ QFile f(filename);
+ if (f.exists() && f.remove()) {
+ emit screenshotRemoved(filename);
+ }
+}
+
+QStringList ScreenshotEndpoint::screenshots() const
+{
+ QDir dir(m_pebble->storagePath() + "/screenshots/");
+ QStringList ret;
+ foreach (const QString &filename, dir.entryList(QDir::Files)) {
+ ret << m_pebble->storagePath() + "/screenshots/" + filename;
+ }
+
+ return ret;
+}
+
+void ScreenshotEndpoint::handleScreenshotData(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ int offset = 0;
+
+ if (m_waitingForMore == 0) {
+
+ ResponseCode responseCode = (ResponseCode)reader.read<quint8>();
+ if (responseCode != ResponseCodeOK) {
+ qWarning() << "Error taking screenshot:" << responseCode;
+ return;
+ }
+ m_version = reader.read<quint32>();
+
+ m_width = reader.read<quint32>();
+ m_height = reader.read<quint32>();
+
+ switch (m_version) {
+ case 1:
+ m_waitingForMore = m_width * m_height / 8;
+ break;
+ case 2:
+ m_waitingForMore = m_width * m_height;
+ break;
+ default:
+ qWarning() << "Unsupported screenshot format version";
+ m_waitingForMore = m_width * m_height; // might work :)
+ }
+
+ offset = 13;
+ m_accumulatedData.clear();
+ }
+
+ QByteArray tmp = reader.readBytes(data.length() - offset);
+ m_waitingForMore -= tmp.length();
+ m_accumulatedData.append(tmp);
+
+ if (m_waitingForMore == 0) {
+ QByteArray output;
+ switch (m_version) {
+ case 1: {
+ int rowBytes = m_width / 8;
+ for (quint32 row = 0; row < m_height; row++) {
+ for (quint32 col = 0; col < m_width; col++) {
+ char pixel = (m_accumulatedData.at(row * rowBytes + col / 8) >> (col % 8)) & 1;
+ output.append(pixel * 255);
+ output.append(pixel * 255);
+ output.append(pixel * 255);
+ }
+ }
+ break;
+ }
+ case 2:
+ for (quint32 row = 0; row < m_height; row++) {
+ for (quint32 col = 0; col < m_width; col++) {
+ char pixel = m_accumulatedData.at(row * m_width + col);
+ output.append(((pixel >> 4) & 0b11) * 85);
+ output.append(((pixel >> 2) & 0b11) * 85);
+ output.append(((pixel >> 0) & 0b11) * 85);
+ }
+ }
+ break;
+ default:
+ qWarning() << "Invalid format.";
+ return;
+ }
+
+ QImage image = QImage((uchar*)output.data(), m_width, m_height, QImage::Format_RGB888);
+ QDir dir(m_pebble->storagePath() + "/screenshots/");
+ if (!dir.exists()) {
+ dir.mkpath(dir.absolutePath());
+ }
+ QString filename = dir.absolutePath() + "/" + QDateTime::currentDateTime().toString("yyyyMMddHHmmss") + ".jpg";
+ image.save(filename);
+ qDebug() << "Screenshot saved to" << filename;
+ emit screenshotAdded(filename);
+ }
+}
+
+
+QByteArray ScreenshotRequestPackage::serialize() const
+{
+ QByteArray data;
+ WatchDataWriter writer(&data);
+
+ writer.write<quint8>(m_command);
+ return data;
+}
diff --git a/rockworkd/libpebble/screenshotendpoint.h b/rockworkd/libpebble/screenshotendpoint.h
new file mode 100644
index 0000000..cca6cfd
--- /dev/null
+++ b/rockworkd/libpebble/screenshotendpoint.h
@@ -0,0 +1,52 @@
+#ifndef SCREENSHOTENDPOINT_H
+#define SCREENSHOTENDPOINT_H
+
+#include <QObject>
+
+#include "watchconnection.h"
+class Pebble;
+
+class ScreenshotRequestPackage: public PebblePacket
+{
+public:
+ QByteArray serialize() const override;
+private:
+ quint8 m_command = 0x00;
+};
+
+class ScreenshotEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum ResponseCode {
+ ResponseCodeOK = 0,
+ ResponseCodeMalformedCommand = 1,
+ ResponseCodeOutOfMemory = 2,
+ ResponseCodeAlreadyInProgress = 3
+ };
+
+ explicit ScreenshotEndpoint(Pebble *pebble, WatchConnection *connection, QObject *parent = 0);
+
+ void requestScreenshot();
+ void removeScreenshot(const QString &filename);
+
+ QStringList screenshots() const;
+
+signals:
+ void screenshotAdded(const QString &filename);
+ void screenshotRemoved(const QString &filename);
+
+private slots:
+ void handleScreenshotData(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ quint32 m_waitingForMore = 0;
+ quint32 m_version = 0;
+ quint32 m_width = 0;
+ quint32 m_height = 0;
+ QByteArray m_accumulatedData;
+};
+
+#endif // SCREENSHOTENDPOINT_H
diff --git a/rockworkd/libpebble/timelineitem.cpp b/rockworkd/libpebble/timelineitem.cpp
new file mode 100644
index 0000000..4bc699c
--- /dev/null
+++ b/rockworkd/libpebble/timelineitem.cpp
@@ -0,0 +1,144 @@
+#include "timelineitem.h"
+
+TimelineItem::TimelineItem(TimelineItem::Type type, Flags flags, const QDateTime &timestamp, quint16 duration):
+ TimelineItem(QUuid::createUuid(), type, flags, timestamp, duration)
+{
+
+}
+
+TimelineItem::TimelineItem(const QUuid &uuid, TimelineItem::Type type, Flags flags, const QDateTime &timestamp, quint16 duration):
+ PebblePacket(),
+ m_itemId(uuid),
+ m_timestamp(timestamp),
+ m_duration(duration),
+ m_type(type),
+ m_flags(flags)
+{
+
+}
+
+QUuid TimelineItem::itemId() const
+{
+ return m_itemId;
+}
+
+void TimelineItem::setLayout(quint8 layout)
+{
+ m_layout = layout;
+}
+
+void TimelineItem::setFlags(Flags flags)
+{
+ m_flags = flags;
+}
+
+void TimelineItem::appendAttribute(const TimelineAttribute &attribute)
+{
+ m_attributes.append(attribute);
+}
+
+void TimelineItem::appendAction(const TimelineAction &action)
+{
+ m_actions.append(action);
+}
+
+QList<TimelineAttribute> TimelineItem::attributes() const
+{
+ return m_attributes;
+}
+
+QList<TimelineAction> TimelineItem::actions() const
+{
+ return m_actions;
+}
+
+QByteArray TimelineItem::serialize() const
+{
+ QByteArray ret;
+ ret.append(m_itemId.toRfc4122());
+ ret.append(m_parentId.toRfc4122());
+ int ts = m_timestamp.toMSecsSinceEpoch() / 1000;
+ ret.append(ts & 0xFF); ret.append((ts >> 8) & 0xFF); ret.append((ts >> 16) & 0xFF); ret.append((ts >> 24) & 0xFF);
+ ret.append(m_duration & 0xFF); ret.append(((m_duration >> 8) & 0xFF));
+ ret.append((quint8)m_type);
+ ret.append(m_flags & 0xFF); ret.append(((m_flags >> 8) & 0xFF));
+ ret.append(m_layout);
+
+ QByteArray serializedAttributes;
+ foreach (const TimelineAttribute &attribute, m_attributes) {
+ serializedAttributes.append(attribute.serialize());
+ }
+
+ QByteArray serializedActions;
+ foreach (const TimelineAction &action, m_actions) {
+ serializedActions.append(action.serialize());
+ }
+ quint16 dataLength = serializedAttributes.length() + serializedActions.length();
+ ret.append(dataLength & 0xFF); ret.append(((dataLength >> 8) & 0xFF));
+ ret.append(m_attributes.count());
+ ret.append(m_actions.count());
+ ret.append(serializedAttributes);
+ ret.append(serializedActions);
+ return ret;
+}
+
+TimelineAction::TimelineAction(quint8 actionId, TimelineAction::Type type, const QList<TimelineAttribute> &attributes):
+ PebblePacket(),
+ m_actionId(actionId),
+ m_type(type),
+ m_attributes(attributes)
+{
+
+}
+
+void TimelineAction::appendAttribute(const TimelineAttribute &attribute)
+{
+ m_attributes.append(attribute);
+}
+
+void TimelineAttribute::setContent(const QString &content)
+{
+ m_content = content.toUtf8();
+}
+
+void TimelineAttribute::setContent(TimelineAttribute::IconID iconId)
+{
+ m_content.clear();
+ m_content.append((quint8)iconId);
+ m_content.append('\0');
+ m_content.append('\0');
+ m_content.append(0x80);
+}
+
+void TimelineAttribute::setContent(TimelineAttribute::Color color)
+{
+ m_content.clear();
+ m_content.append((quint8)color);
+}
+
+void TimelineAttribute::setContent(const QStringList &values)
+{
+ m_content.clear();
+ foreach (const QString &value, values) {
+ if (!m_content.isEmpty()) {
+ m_content.append('\0');
+ }
+ m_content.append(value.toUtf8());
+ }
+}
+
+void TimelineAttribute::setContent(quint8 data)
+{
+ m_content.clear();
+ m_content.append(data);
+}
+
+QByteArray TimelineAttribute::serialize() const
+{
+ QByteArray ret;
+ ret.append((quint8)m_type);
+ ret.append(m_content.length() & 0xFF); ret.append(((m_content.length() >> 8) & 0xFF)); // length
+ ret.append(m_content);
+ return ret;
+}
+
diff --git a/rockworkd/libpebble/timelineitem.h b/rockworkd/libpebble/timelineitem.h
new file mode 100644
index 0000000..ed35539
--- /dev/null
+++ b/rockworkd/libpebble/timelineitem.h
@@ -0,0 +1,194 @@
+#ifndef TIMELINEITEM_H
+#define TIMELINEITEM_H
+
+#include <QByteArray>
+#include <QDateTime>
+
+#include "watchconnection.h"
+
+
+class TimelineAttribute
+{
+public:
+ enum Type {
+ TypeTitle = 0x01,
+ TypeSubtitle = 0x02,
+ TypeBody = 0x03,
+ TypeTinyIcon = 0x04,
+ TypeLargeIcon = 0x06,
+ TypeFieldNames = 0x19,
+ TypeFieldValues = 0x1a,
+ TypeColor = 0x1c,
+ TypeRecurring = 0x1f
+ };
+ enum IconID {
+ IconIDDefaultBell = 0x01,
+ IconIDDefaultMissedCall = 0x02,
+ IconIDReminder = 0x03,
+ IconIDFlag = 0x04,
+ IconIDWhatsApp = 0x05,
+ IconIDTwitter = 0x06,
+ IconIDTelegram = 0x07,
+ IconIDHangout = 0x08,
+ IconIDGMail = 0x09,
+ IconIDFlash = 0x0a, // TODO: what service is this?
+ IconIDFacebook = 0x0b,
+ IconIDMusic = 0x0c,
+ IconIDAlarm = 0x0d,
+ IconIDWeather = 0x0e,
+ IconIDGuess = 0x31
+ };
+
+ enum Color {
+ ColorWhite = 0x00,
+ ColorBlack = 0x80,
+ ColorDarkBlue = 0x81,
+ ColorBlue = 0x82,
+ ColorLightBlue = 0x83,
+ ColorDarkGreen = 0x84,
+ ColorGray = 0x85,
+ ColorBlue2 = 0x86,
+ ColorLightBlue2 = 0x87,
+ ColorGreen = 0x88,
+ ColorOliveGreen = 0x89,
+ ColorLightGreen = 0x90,
+ ColorViolet = 0x91,
+ ColorViolet2 = 0x91,
+ ColorBlue3 = 0x92,
+ ColorBrown = 0x93,
+ ColorGray2 = 0x94,
+ ColorBlue4 = 0x95,
+ ColorBlue5 = 0x96,
+ ColorRed = 0xA0,
+ ColorOrange = 0xB8,
+ ColorYellow = 0xBC
+ };
+
+ TimelineAttribute(Type type, const QByteArray &content):
+ m_type(type),
+ m_content(content)
+ {}
+
+ TimelineAttribute(Type type, IconID iconId):
+ m_type(type)
+ {
+ setContent(iconId);
+ }
+ TimelineAttribute(Type type, Color color):
+ m_type(type)
+ {
+ setContent(color);
+ }
+ TimelineAttribute(Type type, const QStringList &values):
+ m_type(type)
+ {
+ setContent(values);
+ }
+ TimelineAttribute(Type type, quint8 data):
+ m_type(type)
+ {
+ setContent(data);
+ }
+
+ void setContent(const QString &content);
+ void setContent(IconID iconId);
+ void setContent(Color color);
+ void setContent(const QStringList &values);
+ void setContent(quint8 data);
+
+ QByteArray serialize() const;
+private:
+ Type m_type;
+ QByteArray m_content;
+};
+
+class TimelineAction: public PebblePacket
+{
+public:
+ enum Type {
+ TypeAncsDismiss = 1,
+ TypeGeneric = 2,
+ TypeResponse = 3,
+ TypeDismiss = 4,
+ TypeHTTP = 5,
+ TypeSnooze = 6,
+ TypeOpenWatchApp = 7,
+ TypeEmpty = 8,
+ TypeRemove = 9,
+ TypeOpenPin = 10
+ };
+ TimelineAction(quint8 actionId, Type type, const QList<TimelineAttribute> &attributes = QList<TimelineAttribute>());
+ void appendAttribute(const TimelineAttribute &attribute);
+
+ QByteArray serialize() const override {
+ QByteArray ret;
+ ret.append(m_actionId);
+ ret.append((quint8)m_type);
+ ret.append(m_attributes.count());
+ foreach (const TimelineAttribute &attr, m_attributes) {
+ ret.append(attr.serialize());
+ }
+ return ret;
+ }
+
+private:
+ quint8 m_actionId;
+ Type m_type;
+ QList<TimelineAttribute> m_attributes;
+};
+
+class TimelineItem: public PebblePacket
+{
+public:
+ enum Type {
+ TypeNotification = 1,
+ TypePin = 2,
+ TypeReminder = 3
+ };
+
+ // TODO: this is probably not complete and maybe even wrong.
+ enum Flag {
+ FlagNone = 0x00,
+ FlagSingleEvent = 0x01,
+ FlagTimeInUTC = 0x02,
+ FlagAllDay = 0x04
+ };
+ Q_DECLARE_FLAGS(Flags, Flag)
+
+ // TODO: This is not complete
+ enum Layout {
+ LayoutGenericPin = 0x01,
+ LayoutCalendar = 0x02
+ };
+
+ TimelineItem(Type type, TimelineItem::Flags flags = FlagNone, const QDateTime &timestamp = QDateTime::currentDateTime(), quint16 duration = 0);
+ TimelineItem(const QUuid &uuid, Type type, Flags flags = FlagNone, const QDateTime &timestamp = QDateTime::currentDateTime(), quint16 duration = 0);
+
+ QUuid itemId() const;
+
+ void setLayout(quint8 layout);
+ void setFlags(Flags flags);
+
+ void appendAttribute(const TimelineAttribute &attribute);
+ void appendAction(const TimelineAction &action);
+
+ QList<TimelineAttribute> attributes() const;
+ QList<TimelineAction> actions() const;
+
+ QByteArray serialize() const override;
+
+private:
+ QUuid m_itemId;
+ QUuid m_parentId;
+ QDateTime m_timestamp;
+ quint16 m_duration = 0;
+ Type m_type;
+ Flags m_flags; // quint16
+ quint8 m_layout = 0x01; // TODO: find out what this is about
+ QList<TimelineAttribute> m_attributes;
+ QList<TimelineAction> m_actions;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineItem::Flags)
+
+#endif // TIMELINEITEM_H
diff --git a/rockworkd/libpebble/uploadmanager.cpp b/rockworkd/libpebble/uploadmanager.cpp
new file mode 100644
index 0000000..6c6860f
--- /dev/null
+++ b/rockworkd/libpebble/uploadmanager.cpp
@@ -0,0 +1,331 @@
+#include "uploadmanager.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+static const int CHUNK_SIZE = 2000;
+
+UploadManager::UploadManager(WatchConnection *connection, QObject *parent) :
+ QObject(parent), m_connection(connection),
+ _lastUploadId(0), _state(StateNotStarted)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointPutBytes, this, "handlePutBytesMessage");
+}
+
+uint UploadManager::upload(WatchConnection::UploadType type, int index, quint32 appInstallId, const QString &filename, int size, quint32 crc,
+ SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ qDebug() << "Should enqueue uplodad:" << filename;
+ PendingUpload upload;
+ upload.id = ++_lastUploadId;
+ upload.type = type;
+ upload.index = index;
+ upload.filename = filename;
+ upload.appInstallId = appInstallId;
+ QFile *f = new QFile(filename);
+ if (!f->open(QFile::ReadOnly)) {
+ qWarning() << "Error opening file" << filename << "for reading. Cannot upload file";
+ if (errorCallback) {
+ errorCallback(-1);
+ }
+ }
+ upload.device = f;
+ if (size < 0) {
+ upload.size = f->size();
+ } else {
+ upload.size = size;
+ }
+ upload.remaining = upload.size;
+ upload.crc = crc;
+ upload.successCallback = successCallback;
+ upload.errorCallback = errorCallback;
+ upload.progressCallback = progressCallback;
+
+ if (upload.remaining <= 0) {
+ qWarning() << "upload is empty";
+ if (errorCallback) {
+ errorCallback(-1);
+ return -1;
+ }
+ }
+
+ _pending.enqueue(upload);
+
+ if (_pending.size() == 1) {
+ startNextUpload();
+ }
+
+ return upload.id;
+}
+
+uint UploadManager::uploadAppBinary(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeBinary, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadAppResources(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeResources, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadFile(const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeFile, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadFirmwareBinary(bool recovery, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(recovery ? WatchConnection::UploadTypeRecovery: WatchConnection::UploadTypeFirmware, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadFirmwareResources(const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeSystemResources, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadAppWorker(quint32 appInstallId, const QString &filename, quint32 crc, UploadManager::SuccessCallback successCallback, UploadManager::ErrorCallback errorCallback, UploadManager::ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeWorker, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+void UploadManager::cancel(uint id, int code)
+{
+ if (_pending.empty()) {
+ qWarning() << "cannot cancel, empty queue";
+ return;
+ }
+
+ if (id == _pending.head().id) {
+ PendingUpload upload = _pending.dequeue();
+ qDebug() << "aborting current upload" << id << "(code:" << code << ")";
+
+ if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) {
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandAbort);
+ writer.write<quint32>(_token);
+
+ qDebug() << "sending abort for upload" << id;
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+ }
+
+ _state = StateNotStarted;
+ _token = 0;
+
+ if (upload.errorCallback) {
+ upload.errorCallback(code);
+ }
+ upload.device->deleteLater();
+
+ if (!_pending.empty()) {
+ startNextUpload();
+ }
+ } else {
+ for (int i = 1; i < _pending.size(); ++i) {
+ if (_pending[i].id == id) {
+ qDebug() << "cancelling upload" << id << "(code:" << code << ")";
+ if (_pending[i].errorCallback) {
+ _pending[i].errorCallback(code);
+ }
+ _pending.at(i).device->deleteLater();
+ _pending.removeAt(i);
+ return;
+ }
+ }
+ qWarning() << "cannot cancel, id" << id << "not found";
+ }
+}
+
+void UploadManager::startNextUpload()
+{
+ Q_ASSERT(!_pending.empty());
+ Q_ASSERT(_state == StateNotStarted);
+
+ PendingUpload &upload = _pending.head();
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandInit);
+ writer.write<quint32>(upload.remaining);
+ if (upload.index != -1) {
+ writer.write<quint8>(upload.type);
+ writer.write<quint8>(upload.index);
+ if (!upload.filename.isEmpty()) {
+ writer.writeCString(upload.filename);
+ }
+ } else {
+ writer.write<quint8>(upload.type|0x80);
+ writer.writeLE<quint32>(upload.appInstallId);
+ }
+
+ qDebug().nospace() << "starting new upload " << upload.id
+ << ", size:" << upload.remaining
+ << ", type:" << upload.type
+ << ", slot:" << upload.index
+ << ", crc:" << upload.crc
+ << ", filename:" << upload.filename;
+
+ qDebug() << msg.toHex();
+
+ _state = StateWaitForToken;
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+}
+
+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!
+ qWarning() << "short read during upload" << upload.id;
+ return false;
+ }
+
+ Q_ASSERT(!chunk.isEmpty());
+ Q_ASSERT(_state = StateInProgress);
+
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandSend);
+ writer.write<quint32>(_token);
+ writer.write<quint32>(chunk.size());
+ msg.append(chunk);
+
+ qDebug() << "sending a chunk of" << chunk.size() << "bytes";
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+
+ upload.remaining -= chunk.size();
+
+ qDebug() << "remaining" << upload.remaining << "/" << upload.size << "bytes";
+
+ return true;
+}
+
+bool UploadManager::commit(PendingUpload &upload)
+{
+ Q_ASSERT(_state == StateCommit);
+ Q_ASSERT(upload.remaining == 0);
+
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandCommit);
+ writer.write<quint32>(_token);
+ writer.write<quint32>(upload.crc);
+
+ qDebug() << "commiting upload" << upload.id;
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+
+ return true;
+}
+
+bool UploadManager::complete(PendingUpload &upload)
+{
+ Q_ASSERT(_state == StateComplete);
+
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandComplete);
+ writer.write<quint32>(_token);
+
+ qDebug() << "completing upload" << upload.id;
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+
+ return true;
+}
+
+void UploadManager::handlePutBytesMessage(const QByteArray &data)
+{
+ if (_pending.empty()) {
+ qWarning() << "putbytes message, but queue is empty!";
+ return;
+ }
+ Q_ASSERT(!_pending.empty());
+ PendingUpload &upload = _pending.head();
+
+ WatchDataReader reader(data);
+ int status = reader.read<quint8>();
+
+ if (reader.bad() || status != 1) {
+ qWarning() << "upload" << upload.id << "got error code=" << status;
+ cancel(upload.id, status);
+ return;
+ }
+
+ quint32 recv_token = reader.read<quint32>();
+
+ if (reader.bad()) {
+ qWarning() << "upload" << upload.id << ": could not read the token";
+ cancel(upload.id, -1);
+ return;
+ }
+
+ if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) {
+ if (recv_token != _token) {
+ qWarning() << "upload" << upload.id << ": invalid token";
+ cancel(upload.id, -1);
+ return;
+ }
+ }
+
+ switch (_state) {
+ case StateNotStarted:
+ qWarning() << "got packet when upload is not started";
+ break;
+ case StateWaitForToken:
+ qDebug() << "token received";
+ _token = recv_token;
+ _state = StateInProgress;
+
+ /* fallthrough */
+ case StateInProgress:
+ qDebug() << "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 {
+ qDebug() << "no additional chunks, commit";
+ _state = StateCommit;
+ if (!commit(upload)) {
+ cancel(upload.id, -1);
+ return;
+ }
+ }
+ break;
+ case StateCommit:
+ qDebug() << "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:
+ qDebug() << "upload" << upload.id << "succesful, invoking callback";
+ if (upload.successCallback) {
+ upload.successCallback();
+ }
+ upload.device->deleteLater();
+ _pending.dequeue();
+ _token = 0;
+ _state = StateNotStarted;
+ if (!_pending.empty()) {
+ startNextUpload();
+ }
+ break;
+ default:
+ qWarning() << "received message in wrong state";
+ break;
+ }
+}
diff --git a/rockworkd/libpebble/uploadmanager.h b/rockworkd/libpebble/uploadmanager.h
new file mode 100644
index 0000000..a717417
--- /dev/null
+++ b/rockworkd/libpebble/uploadmanager.h
@@ -0,0 +1,85 @@
+#ifndef UPLOADMANAGER_H
+#define UPLOADMANAGER_H
+
+#include <functional>
+#include <QQueue>
+#include "watchconnection.h"
+
+class UploadManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit UploadManager(WatchConnection *watch, QObject *parent = 0);
+
+ typedef std::function<void()> SuccessCallback;
+ typedef std::function<void(int)> ErrorCallback;
+ typedef std::function<void(qreal)> ProgressCallback;
+
+ uint upload(WatchConnection::UploadType type, int index, quint32 appInstallId, const QString &filename, int size = -1, quint32 crc = 0,
+ SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ uint uploadAppBinary(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+ uint uploadAppResources(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+ uint uploadAppWorker(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ uint uploadFirmwareBinary(bool recovery, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+ uint uploadFirmwareResources(const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ uint uploadFile(const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ void cancel(uint id, int code = 0);
+
+signals:
+
+private:
+ enum State {
+ StateNotStarted,
+ StateWaitForToken,
+ StateInProgress,
+ StateCommit,
+ StateComplete
+ };
+
+ struct PendingUpload {
+ uint id;
+
+ WatchConnection::UploadType type;
+ int index = -1;
+ QString filename;
+ quint32 appInstallId;
+ QIODevice *device;
+ int size;
+ int remaining;
+ quint32 crc;
+
+ SuccessCallback successCallback;
+ ErrorCallback errorCallback;
+ ProgressCallback progressCallback;
+ };
+
+ enum PutBytesCommand {
+ PutBytesCommandInit = 1,
+ PutBytesCommandSend = 2,
+ PutBytesCommandCommit = 3,
+ PutBytesCommandAbort = 4,
+ PutBytesCommandComplete = 5
+ };
+
+ void startNextUpload();
+ bool uploadNextChunk(PendingUpload &upload);
+ bool commit(PendingUpload &upload);
+ bool complete(PendingUpload &upload);
+
+private slots:
+ void handlePutBytesMessage(const QByteArray &msg);
+
+private:
+ WatchConnection *m_connection;
+ QQueue<PendingUpload> _pending;
+ uint _lastUploadId;
+ State _state;
+ quint32 _token;
+};
+
+#endif // UPLOADMANAGER_H
diff --git a/rockworkd/libpebble/watchconnection.cpp b/rockworkd/libpebble/watchconnection.cpp
new file mode 100644
index 0000000..0778a1d
--- /dev/null
+++ b/rockworkd/libpebble/watchconnection.cpp
@@ -0,0 +1,242 @@
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "uploadmanager.h"
+
+#include <QDBusConnection>
+#include <QDBusReply>
+#include <QDebug>
+#include <QBluetoothAddress>
+#include <QBluetoothLocalDevice>
+#include <QBluetoothSocket>
+#include <QtEndian>
+#include <QDateTime>
+
+WatchConnection::WatchConnection(QObject *parent) :
+ QObject(parent),
+ m_socket(nullptr)
+{
+ m_reconnectTimer.setSingleShot(true);
+ QObject::connect(&m_reconnectTimer, &QTimer::timeout, this, &WatchConnection::reconnect);
+
+ m_localDevice = new QBluetoothLocalDevice(this);
+ connect(m_localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, &WatchConnection::hostModeStateChanged);
+
+ m_uploadManager = new UploadManager(this, this);
+}
+
+UploadManager *WatchConnection::uploadManager() const
+{
+ return m_uploadManager;
+}
+
+void WatchConnection::scheduleReconnect()
+{
+ if (m_connectionAttempts == 0) {
+ reconnect();
+ } else if (m_connectionAttempts < 25) {
+ qDebug() << "Attempting to reconnect in 10 seconds";
+ m_reconnectTimer.start(1000 * 10);
+ } else if (m_connectionAttempts < 35) {
+ qDebug() << "Attempting to reconnect in 1 minute";
+ m_reconnectTimer.start(1000 * 60);
+ } else {
+ qDebug() << "Attempting to reconnect in 15 minutes";
+ m_reconnectTimer.start(1000 * 60 * 15);
+ }
+}
+
+void WatchConnection::reconnect()
+{
+ QBluetoothLocalDevice localBtDev;
+ if (localBtDev.pairingStatus(m_pebbleAddress) == QBluetoothLocalDevice::Unpaired) {
+ // Try again in one 10 secs, give the user some time to pair it
+ m_connectionAttempts = 1;
+ scheduleReconnect();
+ return;
+ }
+
+ if (m_socket) {
+ if (m_socket->state() == QBluetoothSocket::ConnectedState) {
+ qDebug() << "Already connected.";
+ return;
+ }
+ delete m_socket;
+ }
+
+ m_socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this);
+ connect(m_socket, &QBluetoothSocket::connected, this, &WatchConnection::pebbleConnected);
+ connect(m_socket, &QBluetoothSocket::readyRead, this, &WatchConnection::readyRead);
+ connect(m_socket, SIGNAL(error(QBluetoothSocket::SocketError)), this, SLOT(socketError(QBluetoothSocket::SocketError)));
+ connect(m_socket, &QBluetoothSocket::disconnected, this, &WatchConnection::pebbleDisconnected);
+ //connect(socket, SIGNAL(bytesWritten(qint64)), SLOT(onBytesWritten(qint64)));
+
+ m_connectionAttempts++;
+
+ // FIXME: Assuming port 1 (with Pebble)
+ m_socket->connectToService(m_pebbleAddress, 1);
+}
+
+void WatchConnection::connectPebble(const QBluetoothAddress &pebble)
+{
+ m_pebbleAddress = pebble;
+ m_connectionAttempts = 0;
+ scheduleReconnect();
+}
+
+bool WatchConnection::isConnected()
+{
+ return m_socket && m_socket->state() == QBluetoothSocket::ConnectedState;
+}
+
+void WatchConnection::writeToPebble(Endpoint endpoint, const QByteArray &data)
+{
+ if (!m_socket || m_socket->state() != QBluetoothSocket::ConnectedState) {
+ qWarning() << "Socket not open. Cannot send data to Pebble. (Endpoint:" << endpoint << ")";
+ return;
+ }
+
+ //qDebug() << "sending message to endpoint" << endpoint;
+ QByteArray msg;
+
+ msg.append((data.length() & 0xFF00) >> 8);
+ msg.append(data.length() & 0xFF);
+
+ msg.append((endpoint & 0xFF00) >> 8);
+ msg.append(endpoint & 0xFF);
+
+ msg.append(data);
+
+ //qDebug() << "Writing:" << msg.toHex();
+ m_socket->write(msg);
+}
+
+void WatchConnection::systemMessage(WatchConnection::SystemMessage msg)
+{
+ QByteArray data;
+ data.append((char)0);
+ data.append((char)msg);
+ writeToPebble(EndpointSystemMessage, data);
+}
+
+bool WatchConnection::registerEndpointHandler(WatchConnection::Endpoint endpoint, QObject *handler, const QString &method)
+{
+ if (m_endpointHandlers.contains(endpoint)) {
+ qWarning() << "Already have a handlder for endpoint" << endpoint;
+ return false;
+ }
+ Callback cb;
+ cb.obj = handler;
+ cb.method = method;
+ m_endpointHandlers.insert(endpoint, cb);
+ return true;
+}
+
+void WatchConnection::pebbleConnected()
+{
+ m_connectionAttempts = 0;
+ emit watchConnected();
+}
+
+void WatchConnection::pebbleDisconnected()
+{
+ qDebug() << "Disconnected";
+ m_socket->close();
+ emit watchDisconnected();
+ if (!m_reconnectTimer.isActive()) {
+ scheduleReconnect();
+ }
+}
+
+void WatchConnection::socketError(QBluetoothSocket::SocketError error)
+{
+ Q_UNUSED(error); // We seem to get UnknownError anyways all the time
+ qDebug() << "SocketError" << error;
+ m_socket->close();
+ emit watchConnectionFailed();
+ if (!m_reconnectTimer.isActive()) {
+ scheduleReconnect();
+ }
+}
+
+void WatchConnection::readyRead()
+{
+// QByteArray data = m_socket->readAll();
+// qDebug() << "data from pebble" << data.toHex();
+
+// QByteArray header = data.left(4);
+// qDebug() << "header:" << header.toHex();
+ if (!m_socket) {
+ return;
+ }
+ int headerLength = 4;
+ uchar header[4];
+ m_socket->peek(reinterpret_cast<char*>(header), headerLength);
+
+ quint16 messageLength = qFromBigEndian<quint16>(&header[0]);
+ Endpoint endpoint = (Endpoint)qFromBigEndian<quint16>(&header[2]);
+
+ if (m_socket->bytesAvailable() < headerLength + messageLength) {
+// qDebug() << "not enough data... waiting for more";
+ return;
+ }
+
+ QByteArray data = m_socket->read(headerLength + messageLength);
+// qDebug() << "Have message for endpoint:" << endpoint << "data:" << data.toHex();
+
+ data = data.right(data.length() - 4);
+
+ if (m_endpointHandlers.contains(endpoint)) {
+ if (m_endpointHandlers.contains(endpoint)) {
+ Callback cb = m_endpointHandlers.value(endpoint);
+ QMetaObject::invokeMethod(cb.obj.data(), cb.method.toLatin1(), Q_ARG(QByteArray, data));
+ }
+ } else {
+ qWarning() << "Have message for unhandled endpoint" << endpoint << data.toHex();
+ }
+
+ if (m_socket->bytesAvailable() > 0) {
+ readyRead();
+ }
+}
+
+void WatchConnection::hostModeStateChanged(QBluetoothLocalDevice::HostMode state)
+{
+ switch (state) {
+ case QBluetoothLocalDevice::HostPoweredOff:
+ qDebug() << "Bluetooth turned off. Stopping any reconnect attempts.";
+ m_reconnectTimer.stop();
+ break;
+ case QBluetoothLocalDevice::HostConnectable:
+ case QBluetoothLocalDevice::HostDiscoverable:
+ case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry:
+ if (m_socket && m_socket->state() != QBluetoothSocket::ConnectedState
+ && m_socket->state() != QBluetoothSocket::ConnectingState
+ && !m_reconnectTimer.isActive()) {
+ qDebug() << "Bluetooth now active. Trying to reconnect";
+ m_connectionAttempts = 0;
+ scheduleReconnect();
+ }
+ }
+}
+
+QByteArray WatchConnection::buildData(QStringList data)
+{
+ QByteArray res;
+ for (QString d : data)
+ {
+ QByteArray tmp = d.left(0xEF).toUtf8();
+ res.append((tmp.length() + 1) & 0xFF);
+ res.append(tmp);
+ res.append('\0');
+ }
+ return res;
+}
+
+QByteArray WatchConnection::buildMessageData(uint lead, QStringList data)
+{
+ QByteArray res;
+ res.append(lead & 0xFF);
+ res.append(buildData(data));
+ return res;
+}
diff --git a/rockworkd/libpebble/watchconnection.h b/rockworkd/libpebble/watchconnection.h
new file mode 100644
index 0000000..f2c3d5f
--- /dev/null
+++ b/rockworkd/libpebble/watchconnection.h
@@ -0,0 +1,154 @@
+#ifndef WATCHCONNECTION_H
+#define WATCHCONNECTION_H
+
+#include <QObject>
+#include <QBluetoothAddress>
+#include <QBluetoothSocket>
+#include <QBluetoothLocalDevice>
+#include <QtEndian>
+#include <QPointer>
+#include <QTimer>
+#include <QFile>
+
+class EndpointHandlerInterface;
+class UploadManager;
+
+class PebblePacket {
+public:
+ PebblePacket() {}
+ virtual ~PebblePacket() = default;
+ virtual QByteArray serialize() const = 0;
+ QByteArray packString(const QString &string) const {
+ QByteArray tmp = string.left(0xEF).toUtf8();
+ QByteArray ret;
+ ret.append((tmp.length() + 1) & 0xFF);
+ ret.append(tmp);
+ ret.append('\0');
+ return ret;
+ }
+};
+
+class Callback
+{
+public:
+ QPointer<QObject> obj;
+ QString method;
+};
+
+class WatchConnection : public QObject
+{
+ Q_OBJECT
+public:
+
+ enum Endpoint {
+ EndpointUnknownEndpoint = 0,
+ EndpointTime = 11,
+ EndpointVersion = 16,
+ EndpointPhoneVersion = 17,
+ EndpointSystemMessage = 18,
+ EndpointMusicControl = 32,
+ EndpointPhoneControl = 33,
+ EndpointApplicationMessage = 48,
+ EndpointLauncher = 49,
+ EndpointAppLaunch = 52,
+ EndpointWatchLogs = 2000,
+// EndpointWatchPing = 2001,
+ EndpointLogDump = 2002,
+// EndpointWatchReset = 2003,
+// EndpointWatchApp = 2004,
+// EndpointAppLogs = 2006,
+ EndpointNotification = 3000,
+// watchEXTENSIBLE_NOTIFS = 3010, // Deprecated in 3.x
+// watchRESOURCE = 4000,
+ EndpointFactorySettings = 5001,
+ EndpointAppManager = 6000, // Deprecated in 3.x
+ EndpointAppFetch = 6001, // New in 3.x
+ EndpointDataLogging = 6778,
+ EndpointScreenshot = 8000,
+// watchFILE_MANAGER = 8181,
+// watchCORE_DUMP = 9000,
+// watchAUDIO = 10000, // New in 3.x
+ EndpointActionHandler = 11440,
+ EndpointBlobDB = 45531, // New in 3.x
+ EndpointSorting = 0xabcd,
+ EndpointPutBytes = 0xbeef
+ };
+
+ enum SystemMessage {
+ SystemMessageFirmwareAvailable = 0,
+ SystemMessageFirmwareStart = 1,
+ SystemMessageFirmwareComplete = 2,
+ SystemMessageFirmwareFail = 3,
+ SystemMessageFirmwareUpToDate = 4,
+ SystemMessageFirmwareOutOfDate = 5,
+ SystemMessageBluetoothStartDiscoverable = 6,
+ SystemMessageBluetoothEndDiscoverable = 7
+ };
+
+ typedef QMap<int, QVariant> Dict;
+ enum DictItemType {
+ DictItemTypeBytes,
+ DictItemTypeString,
+ DictItemTypeUInt,
+ DictItemTypeInt
+ };
+
+ enum UploadType {
+ UploadTypeFirmware = 1,
+ UploadTypeRecovery = 2,
+ UploadTypeSystemResources = 3,
+ UploadTypeResources = 4,
+ UploadTypeBinary = 5,
+ UploadTypeFile = 6,
+ UploadTypeWorker = 7
+ };
+ enum UploadStatus {
+ UploadStatusProgress,
+ UploadStatusFailed,
+ UploadStatusSuccess
+ };
+
+ explicit WatchConnection(QObject *parent = 0);
+ UploadManager *uploadManager() const;
+
+ void connectPebble(const QBluetoothAddress &pebble);
+ bool isConnected();
+
+ QByteArray buildData(QStringList data);
+ QByteArray buildMessageData(uint lead, QStringList data);
+
+ void writeToPebble(Endpoint endpoint, const QByteArray &data);
+ void systemMessage(SystemMessage msg);
+
+ bool registerEndpointHandler(Endpoint endpoint, QObject *handler, const QString &method);
+
+signals:
+ void watchConnected();
+ void watchDisconnected();
+ void watchConnectionFailed();
+
+private:
+ void scheduleReconnect();
+ void reconnect();
+
+private slots:
+ void hostModeStateChanged(QBluetoothLocalDevice::HostMode state);
+ void pebbleConnected();
+ void pebbleDisconnected();
+ void socketError(QBluetoothSocket::SocketError error);
+ void readyRead();
+// void logData(const QByteArray &data);
+
+
+private:
+ QBluetoothAddress m_pebbleAddress;
+ QBluetoothLocalDevice *m_localDevice;
+ QBluetoothSocket *m_socket = nullptr;
+ int m_connectionAttempts = 0;
+ QTimer m_reconnectTimer;
+
+ UploadManager *m_uploadManager;
+ QHash<Endpoint, Callback> m_endpointHandlers;
+};
+
+#endif // WATCHCONNECTION_H
diff --git a/rockworkd/libpebble/watchdatareader.cpp b/rockworkd/libpebble/watchdatareader.cpp
new file mode 100644
index 0000000..0c73c73
--- /dev/null
+++ b/rockworkd/libpebble/watchdatareader.cpp
@@ -0,0 +1,6 @@
+#include "watchdatareader.h"
+
+bool WatchDataReader::bad() const
+{
+ return m_bad;
+}
diff --git a/rockworkd/libpebble/watchdatareader.h b/rockworkd/libpebble/watchdatareader.h
new file mode 100644
index 0000000..58e77d8
--- /dev/null
+++ b/rockworkd/libpebble/watchdatareader.h
@@ -0,0 +1,146 @@
+#ifndef WATCHDATAREADER_H
+#define WATCHDATAREADER_H
+
+#include "watchconnection.h"
+
+#include <QByteArray>
+#include <QtEndian>
+#include <QString>
+#include <QUuid>
+#include <QMap>
+
+class WatchDataReader {
+public:
+ WatchDataReader(const QByteArray &data):
+ m_data(data)
+ {
+ }
+
+ template <typename T>
+ T read() {
+ if (checkBad(sizeof(T))) return 0;
+ const uchar *u = p();
+ m_offset += sizeof(T);
+ return qFromBigEndian<T>(u);
+ }
+
+ inline bool checkBad(int n = 0)
+ {
+ if (m_offset + n > m_data.size()) {
+ m_bad = true;
+ }
+ return m_bad;
+ }
+ inline const uchar * p()
+ {
+ return reinterpret_cast<const uchar *>(&m_data.constData()[m_offset]);
+ }
+ inline void skip(int n)
+ {
+ m_offset += n;
+ checkBad();
+ }
+
+ template <typename T>
+ inline T readLE()
+ {
+ if (checkBad(sizeof(T))) return 0;
+ const uchar *u = p();
+ m_offset += sizeof(T);
+ return qFromLittleEndian<T>(u);
+ }
+ QString readFixedString(int n)
+ {
+ if (checkBad(n)) return QString();
+ const char *u = &m_data.constData()[m_offset];
+ m_offset += n;
+ return QString::fromUtf8(u, strnlen(u, n));
+ }
+ QByteArray peek(int n) {
+ return m_data.left(m_offset + n).right(n);
+ }
+ QUuid readUuid()
+ {
+ if (checkBad(16)) return QString();
+ m_offset += 16;
+ return QUuid::fromRfc4122(m_data.mid(m_offset - 16, 16));
+ }
+ QByteArray readBytes(int n)
+ {
+ if (checkBad(n)) return QByteArray();
+ const char *u = &m_data.constData()[m_offset];
+ m_offset += n;
+ return QByteArray(u, n);
+ }
+ QMap<int, QVariant> 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 WatchConnection::DictItemTypeBytes:
+ d.insert(key, QVariant::fromValue(readBytes(width)));
+ break;
+ case WatchConnection::DictItemTypeString:
+ d.insert(key, QVariant::fromValue(readFixedString(width)));
+ break;
+ case WatchConnection::DictItemTypeUInt:
+ 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:
+ m_bad = true;
+ return d;
+ }
+
+ break;
+ case WatchConnection::DictItemTypeInt:
+ 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:
+ m_bad = true;
+ return d;
+ }
+
+ break;
+ default:
+ m_bad = true;
+ return d;
+ }
+ }
+
+ return d;
+ }
+ bool bad() const;
+
+
+private:
+ QByteArray m_data;
+ int m_offset = 0;
+ bool m_bad = false;
+};
+
+#endif // WATCHDATAREADER_H
diff --git a/rockworkd/libpebble/watchdatawriter.cpp b/rockworkd/libpebble/watchdatawriter.cpp
new file mode 100644
index 0000000..e3caf17
--- /dev/null
+++ b/rockworkd/libpebble/watchdatawriter.cpp
@@ -0,0 +1,144 @@
+#include "watchdatawriter.h"
+#include "watchconnection.h"
+
+void WatchDataWriter::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 WatchDataWriter::writeFixedString(int n, const QString &s)
+{
+ _buf->append(s.left(n).toUtf8());
+ for (int i = s.left(n).length(); i < n; i++) {
+ _buf->append('\0');
+ }
+}
+
+void WatchDataWriter::writeCString(const QString &s)
+{
+ _buf->append(s.toUtf8());
+ _buf->append('\0');
+}
+
+void WatchDataWriter::writePascalString(const QString &s)
+{
+ _buf->append(s.length());
+ _buf->append(s.toLatin1());
+}
+
+void WatchDataWriter::writeUuid(const QUuid &uuid)
+{
+ writeBytes(16, uuid.toRfc4122());
+}
+
+void WatchDataWriter::writeDict(const QMap<int, QVariant> &d)
+{
+ int size = d.size();
+ if (size > 0xFF) {
+ qWarning() << "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>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(char));
+ writeLE<char>(it.value().value<char>());
+ break;
+ case QMetaType::Short:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(short));
+ writeLE<short>(it.value().value<short>());
+ break;
+ case QMetaType::Int:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(int));
+ writeLE<int>(it.value().value<int>());
+ break;
+
+ case QMetaType::UChar:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(char));
+ writeLE<char>(it.value().value<char>());
+ break;
+ case QMetaType::UShort:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(short));
+ writeLE<short>(it.value().value<short>());
+ break;
+ case QMetaType::UInt:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(int));
+ writeLE<int>(it.value().value<int>());
+ break;
+
+ case QMetaType::Bool:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(char));
+ writeLE<char>(it.value().value<char>());
+ break;
+
+ case QMetaType::Float: // Treat qreals as ints
+ case QMetaType::Double:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(int));
+ writeLE<int>(it.value().value<int>());
+ break;
+
+ case QMetaType::QByteArray: {
+ QByteArray ba = it.value().toByteArray();
+ writeLE<quint8>(WatchConnection::DictItemTypeBytes);
+ 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>(WatchConnection::DictItemTypeBytes);
+ writeLE<quint16>(ba.size());
+ _buf->append(ba);
+ break;
+ }
+
+ default:
+ qWarning() << "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>(WatchConnection::DictItemTypeString);
+ writeLE<quint16>(s.size());
+ _buf->append(s);
+ break;
+ }
+ }
+ }
+}
diff --git a/rockworkd/libpebble/watchdatawriter.h b/rockworkd/libpebble/watchdatawriter.h
new file mode 100644
index 0000000..8e4adde
--- /dev/null
+++ b/rockworkd/libpebble/watchdatawriter.h
@@ -0,0 +1,69 @@
+#ifndef WATCHDATAWRITER_H
+#define WATCHDATAWRITER_H
+
+#include <QtEndian>
+#include <QByteArray>
+#include <QString>
+#include <QUuid>
+#include <QVariantMap>
+#include <QLoggingCategory>
+
+class WatchDataWriter
+{
+public:
+ WatchDataWriter(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 writePascalString(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 WatchDataWriter::WatchDataWriter(QByteArray *buf)
+ : _buf(buf)
+{
+}
+
+template <typename T>
+void WatchDataWriter::write(T v)
+{
+ qToBigEndian(v, up(sizeof(T)));
+}
+
+template <typename T>
+void WatchDataWriter::writeLE(T v)
+{
+ qToLittleEndian(v, up(sizeof(T)));
+}
+
+inline char * WatchDataWriter::p(int n)
+{
+ int size = _buf->size();
+ _buf->resize(size + n);
+ return &_buf->data()[size];
+}
+
+inline uchar * WatchDataWriter::up(int n)
+{
+ return reinterpret_cast<uchar *>(p(n));
+}
+
+#endif
diff --git a/rockworkd/libpebble/watchlogendpoint.cpp b/rockworkd/libpebble/watchlogendpoint.cpp
new file mode 100644
index 0000000..4b6ab26
--- /dev/null
+++ b/rockworkd/libpebble/watchlogendpoint.cpp
@@ -0,0 +1,128 @@
+#include "watchlogendpoint.h"
+#include "watchdatawriter.h"
+#include "watchdatareader.h"
+#include "pebble.h"
+#include "ziphelper.h"
+
+#include <QDir>
+
+WatchLogEndpoint::WatchLogEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ qsrand(QDateTime::currentMSecsSinceEpoch());
+ m_connection->registerEndpointHandler(WatchConnection::EndpointLogDump, this, "logMessageReceived");
+}
+
+void WatchLogEndpoint::fetchLogs(const QString &fileName)
+{
+ if (m_currentEpoch != 0) {
+ qWarning() << "Already dumping logs. Not starting a second time";
+ return;
+ }
+
+ m_currentFile.setFileName(fileName);
+ if (!m_currentFile.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Cannot open log file for writing" << m_currentFile.fileName();
+ emit logsFetched(false);
+ return;
+ }
+
+ fetchForEpoch(m_currentEpoch);
+}
+
+void WatchLogEndpoint::fetchForEpoch(quint8 epoch)
+{
+ qDebug() << "Dumping logs for epoch" << epoch;
+ QString line("=== Generation: %1 ===\n");
+ line = line.arg(epoch);
+ m_currentFile.write(line.toUtf8());
+ RequestLogPacket packet(WatchLogEndpoint::LogCommandRequestLogs, epoch, qrand());
+ m_connection->writeToPebble(WatchConnection::EndpointLogDump, packet.serialize());
+}
+
+void WatchLogEndpoint::logMessageReceived(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint8 command = reader.read<quint8>();
+ switch (command) {
+ case LogCommandLogMessage: {
+ LogMessage m(data.right(data.length() - 1));
+ QString line("%1 %2 :%3> %4\n");
+ line = line.arg(m.level()).arg(m.timestamp().toString("yyyy-MM-dd hh:mm:ss")).arg(m.line()).arg(m.message());
+ m_currentFile.write(line.toUtf8());
+ break;
+ }
+ case LogCommandLogMessageDone: {
+ qDebug() << "Log for epoch" << m_currentEpoch << "fetched";
+ m_currentEpoch++;
+ if (m_currentEpoch == 0) {
+ // Depending on the capabilities, there might not be a LogCommandNoLogMessages. Make sure we don't cycle endlessly
+ qDebug() << "All 255 epocs fetched. Stopping";
+ m_currentFile.close();
+ emit logsFetched(true);
+ return;
+ }
+ fetchForEpoch(m_currentEpoch);
+ break;
+ }
+ case LogCommandNoLogMessages:
+ qDebug() << "Log dumping finished";
+ m_currentEpoch = 0;
+ m_currentFile.close();
+ emit logsFetched(true);
+ break;
+ default:
+ qWarning() << "LogEndpoint: Unhandled command" << command;
+ }
+}
+
+RequestLogPacket::RequestLogPacket(WatchLogEndpoint::LogCommand command, quint8 generation, quint32 cookie):
+ m_command(command),
+ m_generation(generation),
+ m_cookie(cookie)
+{
+
+}
+
+QByteArray RequestLogPacket::serialize() const
+{
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(m_command);
+ writer.write<quint8>(m_generation);
+ writer.write<quint32>(m_cookie);
+ return msg;
+}
+
+LogMessage::LogMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ m_cookie = reader.read<quint32>();
+ m_timestamp = QDateTime::fromTime_t(reader.read<quint32>());
+ int level = reader.read<quint8>();
+ switch (level) {
+ case 0:
+ m_level = '*';
+ break;
+ case 1:
+ m_level = 'E';
+ break;
+ case 50:
+ m_level = 'W';
+ break;
+ case 100:
+ m_level = 'I';
+ break;
+ case 200:
+ m_level = 'D';
+ case 250:
+ m_level = 'V';
+ }
+
+ m_length = reader.read<quint8>();
+ m_line = reader.read<quint16>();
+ m_filename = reader.readFixedString(16);
+ m_message = reader.readFixedString(m_length);
+}
diff --git a/rockworkd/libpebble/watchlogendpoint.h b/rockworkd/libpebble/watchlogendpoint.h
new file mode 100644
index 0000000..4ce58bf
--- /dev/null
+++ b/rockworkd/libpebble/watchlogendpoint.h
@@ -0,0 +1,76 @@
+#ifndef WATCHLOGENDPOINT_H
+#define WATCHLOGENDPOINT_H
+
+#include <QObject>
+#include <QDateTime>
+
+#include "watchconnection.h"
+
+class Pebble;
+
+class LogMessage: public PebblePacket
+{
+public:
+ LogMessage(const QByteArray &data);
+
+ quint32 cookie() const { return m_cookie; }
+ QDateTime timestamp() const { return m_timestamp; }
+ QChar level() const { return m_level; }
+ quint8 length() const { return m_length; }
+ quint16 line() const { return m_line; }
+ QString filename() const { return m_filename; }
+ QString message() const { return m_message; }
+
+ QByteArray serialize() const override { return QByteArray(); }
+private:
+ quint32 m_cookie;
+ QDateTime m_timestamp;
+ QChar m_level;
+ quint8 m_length;
+ quint16 m_line;
+ QString m_filename;
+ QString m_message;
+};
+
+class WatchLogEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum LogCommand {
+ LogCommandRequestLogs = 0x10,
+ LogCommandLogMessage = 0x80,
+ LogCommandLogMessageDone = 0x81,
+ LogCommandNoLogMessages = 0x82
+ };
+
+ explicit WatchLogEndpoint(Pebble *pebble, WatchConnection *connection);
+
+ void fetchLogs(const QString &fileName);
+
+signals:
+ void logsFetched(bool success);
+
+private slots:
+ void fetchForEpoch(quint8 epoch);
+ void logMessageReceived(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ quint8 m_currentEpoch = 0;
+ QFile m_currentFile;
+ QString m_targetArchive;
+};
+
+class RequestLogPacket: public PebblePacket
+{
+public:
+ RequestLogPacket(WatchLogEndpoint::LogCommand command, quint8 generation, quint32 cookie);
+ QByteArray serialize() const;
+private:
+ WatchLogEndpoint::LogCommand m_command;
+ quint8 m_generation;
+ quint32 m_cookie;
+};
+
+#endif // WATCHLOGENDPOINT_H
diff --git a/rockworkd/libpebble/ziphelper.cpp b/rockworkd/libpebble/ziphelper.cpp
new file mode 100644
index 0000000..f18b8aa
--- /dev/null
+++ b/rockworkd/libpebble/ziphelper.cpp
@@ -0,0 +1,91 @@
+#include "ziphelper.h"
+
+#include <QFileInfo>
+#include <QDebug>
+#include <QDir>
+
+#include <quazip/quazipfile.h>
+#include <quazip/quazip.h>
+
+ZipHelper::ZipHelper()
+{
+
+}
+
+bool ZipHelper::unpackArchive(const QString &archiveFilename, const QString &targetDir)
+{
+ QuaZip zipFile(archiveFilename);
+ if (!zipFile.open(QuaZip::mdUnzip)) {
+ qWarning() << "Failed to open zip file" << zipFile.getZipName();
+ return false;
+ }
+
+ foreach (const QuaZipFileInfo &fi, zipFile.getFileInfoList()) {
+ QuaZipFile f(archiveFilename, fi.name);
+ if (!f.open(QFile::ReadOnly)) {
+ qWarning() << "could not extract file" << fi.name;
+ return false;
+ }
+ if (fi.name.endsWith("/")) {
+ qDebug() << "skipping" << fi.name;
+ continue;
+ }
+ qDebug() << "Inflating:" << fi.name;
+ QFileInfo dirInfo(targetDir + "/" + fi.name);
+ if (!dirInfo.absoluteDir().exists() && !dirInfo.absoluteDir().mkpath(dirInfo.absolutePath())) {
+ qWarning() << "Error creating target dir" << dirInfo.absoluteDir();
+ return false;
+ }
+ QFile of(targetDir + "/" + fi.name);
+ if (!of.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Could not open output file for writing" << fi.name;
+ f.close();
+ return false;
+ }
+ of.write(f.readAll());
+ f.close();
+ of.close();
+ }
+ return true;
+}
+
+bool ZipHelper::packArchive(const QString &archiveFilename, const QString &sourceDir)
+{
+ QuaZip zip(archiveFilename);
+ if (!zip.open(QuaZip::mdCreate)){
+ qWarning() << "Error creating zip file";
+ return false;
+ }
+
+ QDir dir(sourceDir);
+ QuaZipFile outfile(&zip);
+
+ foreach (const QFileInfo &fi, dir.entryInfoList()) {
+ if (!fi.isFile()) {
+ continue;
+ }
+ qDebug() << "have file" << fi.absoluteFilePath();
+ QuaZipNewInfo newInfo(fi.fileName(), fi.absoluteFilePath());
+
+ if (!outfile.open(QFile::WriteOnly, newInfo)) {
+ qWarning() << "Error opening zipfile for writing";
+ zip.close();
+ return false;
+ }
+
+ QFile sourceFile(fi.absoluteFilePath());
+ if (!sourceFile.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening log file for reading" << fi.absoluteFilePath();
+ outfile.close();
+ zip.close();
+ return false;
+ }
+ outfile.write(sourceFile.readAll());
+ outfile.close();
+ sourceFile.close();
+
+ }
+ outfile.close();
+ zip.close();
+ return true;
+}
diff --git a/rockworkd/libpebble/ziphelper.h b/rockworkd/libpebble/ziphelper.h
new file mode 100644
index 0000000..fe3a7a1
--- /dev/null
+++ b/rockworkd/libpebble/ziphelper.h
@@ -0,0 +1,15 @@
+#ifndef ZIPHELPER_H
+#define ZIPHELPER_H
+
+#include <QString>
+
+class ZipHelper
+{
+public:
+ ZipHelper();
+
+ static bool unpackArchive(const QString &archiveFilename, const QString &targetDir);
+ static bool packArchive(const QString &archiveFilename, const QString &sourceDir);
+};
+
+#endif // ZIPHELPER_H
diff --git a/rockworkd/main.cpp b/rockworkd/main.cpp
new file mode 100644
index 0000000..7c58c12
--- /dev/null
+++ b/rockworkd/main.cpp
@@ -0,0 +1,22 @@
+#include <QCoreApplication>
+
+#include "core.h"
+
+#ifdef ENABLE_TESTING
+#include <QGuiApplication>
+#endif
+
+int main(int argc, char *argv[])
+{
+
+#ifdef ENABLE_TESTING
+ QGuiApplication a(argc, argv);
+#else
+ QCoreApplication a(argc, argv);
+#endif
+
+ Core::instance()->init();
+
+ return a.exec();
+}
+
diff --git a/rockworkd/pebblemanager.cpp b/rockworkd/pebblemanager.cpp
new file mode 100644
index 0000000..126000e
--- /dev/null
+++ b/rockworkd/pebblemanager.cpp
@@ -0,0 +1,95 @@
+#include "pebblemanager.h"
+
+#include "core.h"
+
+#include "libpebble/platforminterface.h"
+
+#include <QHash>
+
+#ifdef ENABLE_TESTING
+#include <QQuickView>
+#include <QQmlEngine>
+#include <QQmlContext>
+#endif
+
+PebbleManager::PebbleManager(QObject *parent) : QObject(parent)
+{
+ m_bluezClient = new BluezClient(this);
+ connect(m_bluezClient, &BluezClient::devicesChanged, this, &PebbleManager::loadPebbles);
+ loadPebbles();
+}
+
+QList<Pebble *> PebbleManager::pebbles() const
+{
+ return m_pebbles;
+}
+
+void PebbleManager::loadPebbles()
+{
+ QList<Device> pairedPebbles = m_bluezClient->pairedPebbles();
+ foreach (const Device &device, pairedPebbles) {
+ qDebug() << "loading pebble" << device.address.toString();
+ Pebble *pebble = get(device.address);
+ if (!pebble) {
+ qDebug() << "creating new pebble";
+ pebble = new Pebble(device.address, this);
+ pebble->setName(device.name);
+ setupPebble(pebble);
+ m_pebbles.append(pebble);
+ qDebug() << "have pebbles:" << m_pebbles.count() << this;
+ emit pebbleAdded(pebble);
+ }
+ if (!pebble->connected()) {
+ pebble->connect();
+ }
+ }
+ QList<Pebble*> pebblesToRemove;
+ foreach (Pebble *pebble, m_pebbles) {
+ bool found = false;
+ foreach (const Device &dev, pairedPebbles) {
+ if (dev.address == pebble->address()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ pebblesToRemove << pebble;
+ }
+ }
+
+ while (!pebblesToRemove.isEmpty()) {
+ Pebble *pebble = pebblesToRemove.takeFirst();
+ qDebug() << "Removing pebble" << pebble->address();
+ m_pebbles.removeOne(pebble);
+ emit pebbleRemoved(pebble);
+ pebble->deleteLater();
+ }
+}
+
+void PebbleManager::pebbleConnected()
+{
+}
+
+void PebbleManager::setupPebble(Pebble *pebble)
+{
+
+#ifdef ENABLE_TESTING
+ qmlRegisterUncreatableType<Pebble>("PebbleTest", 1, 0, "Pebble", "Dont");
+ QQuickView *view = new QQuickView();
+ view->engine()->rootContext()->setContextProperty("pebble", pebble);
+ view->setSource(QUrl("qrc:///testui/PebbleController.qml"));
+ view->show();
+#endif
+
+ connect(pebble, &Pebble::pebbleConnected, this, &PebbleManager::pebbleConnected);
+}
+
+Pebble* PebbleManager::get(const QBluetoothAddress &address)
+{
+ for (int i = 0; i < m_pebbles.count(); i++) {
+ if (m_pebbles.at(i)->address() == address) {
+ return m_pebbles.at(i);
+ }
+ }
+ return nullptr;
+}
diff --git a/rockworkd/pebblemanager.h b/rockworkd/pebblemanager.h
new file mode 100644
index 0000000..9387ff7
--- /dev/null
+++ b/rockworkd/pebblemanager.h
@@ -0,0 +1,35 @@
+#ifndef PEBBLEMANAGER_H
+#define PEBBLEMANAGER_H
+
+#include "libpebble/pebble.h"
+#include "libpebble/bluez/bluezclient.h"
+
+#include <QObject>
+
+class PebbleManager : public QObject
+{
+ Q_OBJECT
+public:
+ explicit PebbleManager(QObject *parent = 0);
+
+ QList<Pebble*> pebbles() const;
+ Pebble* get(const QBluetoothAddress &address);
+
+signals:
+ void pebbleAdded(Pebble *pebble);
+ void pebbleRemoved(Pebble *pebble);
+
+private slots:
+ void loadPebbles();
+
+ void pebbleConnected();
+
+private:
+ void setupPebble(Pebble *pebble);
+
+ BluezClient *m_bluezClient;
+
+ QList<Pebble*> m_pebbles;
+};
+
+#endif // PEBBLEMANAGER_H
diff --git a/rockworkd/platformintegration/testing/testingplatform.cpp b/rockworkd/platformintegration/testing/testingplatform.cpp
new file mode 100644
index 0000000..aa0c45a
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testingplatform.cpp
@@ -0,0 +1,63 @@
+#include "testingplatform.h"
+
+#include <QQuickView>
+#include <QDebug>
+#include <QQmlContext>
+
+TestingPlatform::TestingPlatform(QObject *parent):
+ PlatformInterface(parent)
+{
+ m_view = new QQuickView();
+ m_view->rootContext()->setContextProperty("handler", this);
+ qmlRegisterUncreatableType<Pebble>("PebbleTest", 1, 0, "Pebble", "Dont");
+ m_view->setSource(QUrl("qrc:///testui/Main.qml"));
+ m_view->show();
+}
+
+void TestingPlatform::sendMusicControlCommand(MusicControlButton command)
+{
+ qDebug() << "Testing platform received music command from pebble" << command;
+}
+
+MusicMetaData TestingPlatform::musicMetaData() const
+{
+ return MusicMetaData("TestArtist", "TestAlbum", "TestTitle");
+}
+
+void TestingPlatform::sendNotification(int type, const QString &from, const QString &subject, const QString &text)
+{
+ qDebug() << "Injecting mock notification" << type;
+ Notification n("test_app_" + QString::number(type));
+ n.setSourceName("Test button " + QString::number(type));
+ n.setSender(from);
+ n.setSubject(subject);
+ n.setBody(text);
+ n.setActToken("tralala");
+ emit notificationReceived(n);
+}
+
+void TestingPlatform::fakeIncomingCall(uint cookie, const QString &number, const QString &name)
+{
+ emit incomingCall(cookie, number, name);
+}
+
+void TestingPlatform::endCall(uint cookie, bool missed)
+{
+ emit callEnded(cookie, missed);
+}
+
+void TestingPlatform::hangupCall(uint cookie)
+{
+ qDebug() << "Testing platform received a hangup call event";
+ emit callEnded(cookie, false);
+}
+
+QList<CalendarEvent> TestingPlatform::organizerItems() const
+{
+ return QList<CalendarEvent>();
+}
+
+void TestingPlatform::actionTriggered(const QString &actToken)
+{
+ qDebug() << "action triggered" << actToken;
+}
diff --git a/rockworkd/platformintegration/testing/testingplatform.h b/rockworkd/platformintegration/testing/testingplatform.h
new file mode 100644
index 0000000..8c820a0
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testingplatform.h
@@ -0,0 +1,31 @@
+#ifndef TESTINGPLATFORM_H
+#define TESTINGPLATFORM_H
+
+#include "libpebble/platforminterface.h"
+
+class QQuickView;
+
+class TestingPlatform : public PlatformInterface
+{
+ Q_OBJECT
+public:
+ explicit TestingPlatform(QObject *parent = 0);
+
+ void sendMusicControlCommand(MusicControlButton command) override;
+ MusicMetaData musicMetaData() const override;
+
+ Q_INVOKABLE void sendNotification(int type, const QString &from, const QString &subject, const QString &text);
+ Q_INVOKABLE void fakeIncomingCall(uint cookie, const QString &number, const QString &name);
+ Q_INVOKABLE void endCall(uint cookie, bool missed);
+
+ void hangupCall(uint cookie) override;
+
+ QList<CalendarEvent> organizerItems() const override;
+ void actionTriggered(const QString &actToken) override;
+signals:
+
+private:
+ QQuickView *m_view;
+};
+
+#endif // TESTINGPLATFORM_H
diff --git a/rockworkd/platformintegration/testing/testui.qrc b/rockworkd/platformintegration/testing/testui.qrc
new file mode 100644
index 0000000..bc0a45f
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testui.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/">
+ <file>testui/Main.qml</file>
+ <file>testui/PebbleController.qml</file>
+ </qresource>
+</RCC>
diff --git a/rockworkd/platformintegration/testing/testui/Main.qml b/rockworkd/platformintegration/testing/testui/Main.qml
new file mode 100644
index 0000000..e520ca4
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testui/Main.qml
@@ -0,0 +1,87 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.3
+import PebbleTest 1.0
+
+Row {
+ Column {
+ spacing: 10
+ Button {
+ text: "Generic Notification"
+ onClicked: {
+ handler.sendNotification(0, "Bro Coly", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Email Notification"
+ onClicked: {
+ handler.sendNotification(1, "Tom Ato", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "SMS with no subject"
+ onClicked: {
+ handler.sendNotification(2, "Tom Ato", "", "TestText")
+ }
+ }
+
+ Button {
+ text: "Facebook Notification"
+ onClicked: {
+ handler.sendNotification(3, "Cole Raby", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Twitter Notification"
+ onClicked: {
+ handler.sendNotification(4, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Telegram Notification"
+ onClicked: {
+ handler.sendNotification(5, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "WhatsApp Notification"
+ onClicked: {
+ handler.sendNotification(6, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Hangout Notification"
+ onClicked: {
+ handler.sendNotification(7, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+
+ }
+
+ Column {
+ spacing: 10
+ Button {
+ text: "Fake incoming phone call"
+ onClicked: {
+ handler.fakeIncomingCall(1, "123456789", "TestCaller")
+ }
+ }
+ Button {
+ text: "pick up incoming phone call"
+ onClicked: {
+ handler.callStarted(1)
+ }
+ }
+ Button {
+ text: "hang up incoming phone call"
+ onClicked: {
+ handler.endCall(1, false)
+ }
+ }
+ Button {
+ text: "miss incoming phone call"
+ onClicked: {
+ handler.endCall(1, true)
+ }
+ }
+ }
+}
diff --git a/rockworkd/platformintegration/testing/testui/PebbleController.qml b/rockworkd/platformintegration/testing/testui/PebbleController.qml
new file mode 100644
index 0000000..78861d8
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testui/PebbleController.qml
@@ -0,0 +1,44 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.3
+import PebbleTest 1.0
+
+Column {
+ spacing: 10
+ Label {
+ text: pebble.name
+ width: parent.width
+ }
+
+ Button {
+ text: "Insert Timeline Pin"
+ onClicked: {
+ pebble.insertTimelinePin();
+ }
+ }
+ Button {
+ text: "Create Reminder"
+ onClicked: {
+ pebble.insertReminder();
+ }
+ }
+ Button {
+ text: "Clear Timeline"
+ onClicked: {
+ pebble.clearTimeline();
+ }
+ }
+ Button {
+ text: "take screenshot"
+ onClicked: {
+ pebble.requestScreenshot();
+ }
+ }
+
+ Button {
+ text: "dump logs"
+ onClicked: {
+ pebble.dumpLogs();
+ }
+ }
+}
+
diff --git a/rockworkd/platformintegration/ubuntu/callchannelobserver.cpp b/rockworkd/platformintegration/ubuntu/callchannelobserver.cpp
new file mode 100644
index 0000000..e3d852c
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/callchannelobserver.cpp
@@ -0,0 +1,165 @@
+#include "callchannelobserver.h"
+
+#include <TelepathyQt/Contact>
+#include <TelepathyQt/PendingContactInfo>
+
+#include <QContactFetchRequest>
+#include <QContactPhoneNumber>
+#include <QContactFilter>
+#include <QContactDetail>
+#include <QContactDisplayLabel>
+
+QTCONTACTS_USE_NAMESPACE
+
+TelepathyMonitor::TelepathyMonitor(QObject *parent):
+ QObject(parent)
+{
+ Tp::registerTypes();
+ QTimer::singleShot(0, this, &TelepathyMonitor::accountManagerSetup);
+ m_contactManager = new QContactManager("galera");
+ m_contactManager->setParent(this);
+}
+
+void TelepathyMonitor::hangupCall(uint cookie)
+{
+ if (m_currentCalls.contains(cookie)) {
+ m_currentCalls.value(cookie)->hangup();
+ }
+}
+
+void TelepathyMonitor::accountManagerSetup()
+{
+ m_accountManager = Tp::AccountManager::create(Tp::AccountFactory::create(QDBusConnection::sessionBus(),
+ Tp::Account::FeatureCore),
+ Tp::ConnectionFactory::create(QDBusConnection::sessionBus(),
+ Tp::Connection::FeatureCore));
+ connect(m_accountManager->becomeReady(),
+ SIGNAL(finished(Tp::PendingOperation*)),
+ SLOT(accountManagerReady(Tp::PendingOperation*)));
+}
+
+void TelepathyMonitor::accountManagerReady(Tp::PendingOperation* operation)
+{
+ if (operation->isError()) {
+ qDebug() << "TelepathyMonitor: accountManager init error.";
+ QTimer::singleShot(1000, this, &TelepathyMonitor::accountManagerSetup); // again
+ return;
+ }
+ qDebug() << "Telepathy account manager ready";
+
+ foreach (const Tp::AccountPtr& account, m_accountManager->allAccounts()) {
+ connect(account->becomeReady(Tp::Account::FeatureCapabilities),
+ SIGNAL(finished(Tp::PendingOperation*)),
+ SLOT(accountReady(Tp::PendingOperation*)));
+ }
+
+ connect(m_accountManager.data(), SIGNAL(newAccount(Tp::AccountPtr)), SLOT(newAccount(Tp::AccountPtr)));
+}
+
+void TelepathyMonitor::newAccount(const Tp::AccountPtr& account)
+{
+ connect(account->becomeReady(Tp::Account::FeatureCapabilities),
+ SIGNAL(finished(Tp::PendingOperation*)),
+ SLOT(accountReady(Tp::PendingOperation*)));
+}
+
+void TelepathyMonitor::accountReady(Tp::PendingOperation* operation)
+{
+ if (operation->isError()) {
+ qDebug() << "TelepathyAccount: Operation failed (accountReady)";
+ return;
+ }
+
+ Tp::PendingReady* pendingReady = qobject_cast<Tp::PendingReady*>(operation);
+ if (pendingReady == 0) {
+ qDebug() << "Rejecting account because could not understand ready status";
+ return;
+ }
+ checkAndAddAccount(Tp::AccountPtr::qObjectCast(pendingReady->proxy()));
+}
+
+void TelepathyMonitor::onCallStarted(Tp::CallChannelPtr callChannel)
+{
+ // Haven't figured how to send outgoing calls to pebble yet... discard it
+ if (callChannel->initiatorContact()->id().isEmpty()) {
+ qWarning() << "ignoring phone call. looks like it's an outgoing one";
+ return;
+ }
+
+ m_cookie++;
+ m_currentCalls.insert(m_cookie, callChannel.data());
+ m_currentCallStates.insert(m_cookie, Tp::CallStateInitialising);
+
+ callChannel->becomeReady(Tp::CallChannel::FeatureCallState);
+
+ connect(callChannel.data(), &Tp::CallChannel::callStateChanged, this, &TelepathyMonitor::callStateChanged);
+
+ QString number = callChannel->initiatorContact()->id();
+ qDebug() << "call started" << number;
+
+ // try to match the contact info
+ QContactFetchRequest *request = new QContactFetchRequest(this);
+ request->setFilter(QContactPhoneNumber::match(number));
+
+ // lambda function to update the notification
+ QObject::connect(request, &QContactAbstractRequest::stateChanged, [this, request, number](QContactAbstractRequest::State state) {
+ qDebug() << "request returned";
+ if (!request || state != QContactAbstractRequest::FinishedState) {
+ qDebug() << "error fetching contact" << state;
+ return;
+ }
+
+ QContact contact;
+
+ // create the snap decision only after the contact match finishes
+ if (request->contacts().size() > 0) {
+ // use the first match
+ contact = request->contacts().at(0);
+
+ qDebug() << "have contact" << contact.detail<QContactDisplayLabel>().label();
+ emit this->incomingCall(m_cookie, number, contact.detail<QContactDisplayLabel>().label());
+ } else {
+ qDebug() << "unknown contact" << number;
+ emit this->incomingCall(m_cookie, number, QString());
+ }
+ });
+
+ request->setManager(m_contactManager);
+ request->start();
+}
+
+void TelepathyMonitor::callStateChanged(Tp::CallState state)
+{
+ qDebug() << "call state changed1";
+ Tp::CallChannel *channel = qobject_cast<Tp::CallChannel*>(sender());
+ uint cookie = m_currentCalls.key(channel);
+
+ qDebug() << "call state changed2" << state << "cookie:" << cookie;
+
+ switch (state) {
+ case Tp::CallStateActive:
+ emit callStarted(cookie);
+ m_currentCallStates[cookie] = Tp::CallStateActive;
+ break;
+ case Tp::CallStateEnded: {
+ Tp::CallState oldState = m_currentCallStates.value(cookie);
+ emit callEnded(cookie, oldState != Tp::CallStateActive);
+ m_currentCalls.take(cookie);
+ m_currentCallStates.take(cookie);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void TelepathyMonitor::checkAndAddAccount(const Tp::AccountPtr& account)
+{
+ Tp::ConnectionCapabilities caps = account->capabilities();
+ // TODO: Later on we will need to filter for the right capabilities, and also allow dynamic account detection
+ // Don't check caps for now as a workaround for https://bugs.launchpad.net/ubuntu/+source/media-hub/+bug/1409125
+ // at least until we are able to find out the root cause of it (check rev 107 for the caps check)
+ auto tcm = new TelepathyCallMonitor(account);
+ connect(tcm, &TelepathyCallMonitor::callStarted, this, &TelepathyMonitor::onCallStarted);
+ m_callMonitors.append(tcm);
+}
diff --git a/rockworkd/platformintegration/ubuntu/callchannelobserver.h b/rockworkd/platformintegration/ubuntu/callchannelobserver.h
new file mode 100644
index 0000000..cc2b7aa
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/callchannelobserver.h
@@ -0,0 +1,74 @@
+#ifndef CALLCHANNELOBSERVER_H
+#define CALLCHANNELOBSERVER_H
+
+#include <TelepathyQt/AccountManager>
+#include <TelepathyQt/SimpleCallObserver>
+#include <TelepathyQt/PendingOperation>
+#include <TelepathyQt/PendingReady>
+#include <TelepathyQt/PendingAccount>
+#include <TelepathyQt/CallChannel>
+
+#include <QContactManager>
+
+QTCONTACTS_USE_NAMESPACE
+
+class TelepathyCallMonitor : public QObject
+{
+ Q_OBJECT
+public:
+ TelepathyCallMonitor(const Tp::AccountPtr& account):
+ mAccount(account),
+ mCallObserver(Tp::SimpleCallObserver::create(mAccount)) {
+ connect(mCallObserver.data(), SIGNAL(callStarted(Tp::CallChannelPtr)), SIGNAL(callStarted(Tp::CallChannelPtr)));
+// connect(mCallObserver.data(), SIGNAL(callEnded(Tp::CallChannelPtr,QString,QString)), SIGNAL(callEnded()));
+// connect(mCallObserver.data(), SIGNAL(streamedMediaCallStarted(Tp::StreamedMediaChannelPtr)), SIGNAL(offHook()));
+// connect(mCallObserver.data(), SIGNAL(streamedMediaCallEnded(Tp::StreamedMediaChannelPtr,QString,QString)), SIGNAL(onHook()));
+ }
+
+signals:
+ void callStarted(Tp::CallChannelPtr callChannel);
+// void callEnded();
+
+private:
+ Tp::AccountPtr mAccount;
+ Tp::SimpleCallObserverPtr mCallObserver;
+};
+
+class TelepathyMonitor: public QObject
+{
+ Q_OBJECT
+public:
+ TelepathyMonitor(QObject *parent = 0);
+
+ void hangupCall(uint cookie);
+
+private slots:
+ void accountManagerSetup();
+ void accountManagerReady(Tp::PendingOperation* operation);
+
+ void newAccount(const Tp::AccountPtr& account);
+ void accountReady(Tp::PendingOperation* operation);
+
+ void onCallStarted(Tp::CallChannelPtr callChannel);
+ void callStateChanged(Tp::CallState state);
+
+signals:
+ void incomingCall(uint cookie, const QString &number, const QString &name);
+ void callStarted(uint cookie);
+ void callEnded(uint cookie, bool missed);
+
+private:
+ void checkAndAddAccount(const Tp::AccountPtr& account);
+
+private:
+ Tp::AccountManagerPtr m_accountManager;
+ QList<TelepathyCallMonitor*> m_callMonitors;
+ QContactManager *m_contactManager;
+
+ QHash<uint, Tp::CallChannel*> m_currentCalls;
+ QHash<uint, Tp::CallState> m_currentCallStates;
+
+ uint m_cookie = 0;
+};
+
+#endif // CALLCHANNELOBSERVER_H
diff --git a/rockworkd/platformintegration/ubuntu/organizeradapter.cpp b/rockworkd/platformintegration/ubuntu/organizeradapter.cpp
new file mode 100644
index 0000000..853403a
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/organizeradapter.cpp
@@ -0,0 +1,74 @@
+#include "organizeradapter.h"
+
+#include <QOrganizerItemFetchRequest>
+#include <QDebug>
+#include <QOrganizerEventOccurrence>
+#include <QOrganizerItemDetail>
+
+QTORGANIZER_USE_NAMESPACE
+
+#define MANAGER "eds"
+#define MANAGER_FALLBACK "memory"
+
+OrganizerAdapter::OrganizerAdapter(QObject *parent) : QObject(parent)
+{
+ QString envManager(qgetenv("ALARM_BACKEND"));
+ if (envManager.isEmpty())
+ envManager = MANAGER;
+ if (!QOrganizerManager::availableManagers().contains(envManager)) {
+ envManager = MANAGER_FALLBACK;
+ }
+ m_manager = new QOrganizerManager(envManager);
+ m_manager->setParent(this);
+ connect(m_manager, &QOrganizerManager::dataChanged, this, &OrganizerAdapter::refresh);
+}
+
+void OrganizerAdapter::refresh()
+{
+ QList<CalendarEvent> items;
+ foreach (const QOrganizerItem &item, m_manager->items()) {
+ QOrganizerEvent organizerEvent(item);
+ if (organizerEvent.displayLabel().isEmpty()) {
+ continue;
+ }
+ CalendarEvent event;
+ event.setId(organizerEvent.id().toString());
+ event.setTitle(organizerEvent.displayLabel());
+ event.setDescription(organizerEvent.description());
+ event.setStartTime(organizerEvent.startDateTime());
+ event.setEndTime(organizerEvent.endDateTime());
+ event.setLocation(organizerEvent.location());
+ event.setComment(organizerEvent.comments().join(";"));
+ QStringList attendees;
+ foreach (const QOrganizerItemDetail &attendeeDetail, organizerEvent.details(QOrganizerItemDetail::TypeEventAttendee)) {
+ attendees.append(attendeeDetail.value(QOrganizerItemDetail::TypeEventAttendee + 1).toString());
+ }
+ event.setGuests(attendees);
+ event.setRecurring(organizerEvent.recurrenceRules().count() > 0);
+
+ items.append(event);
+
+ quint64 startTimestamp = QDateTime::currentMSecsSinceEpoch();
+ startTimestamp -= (1000 * 60 * 60 * 24 * 7);
+
+ foreach (const QOrganizerItem &occurranceItem, m_manager->itemOccurrences(item, QDateTime::fromMSecsSinceEpoch(startTimestamp), QDateTime::currentDateTime().addDays(7))) {
+ QOrganizerEventOccurrence organizerOccurrance(occurranceItem);
+ event.generateNewUuid();
+ event.setId(organizerOccurrance.id().toString());
+ event.setStartTime(organizerOccurrance.startDateTime());
+ event.setEndTime(organizerOccurrance.endDateTime());
+ items.append(event);
+ }
+ }
+
+ if (m_items != items) {
+ m_items = items;
+ emit itemsChanged(m_items);
+ }
+
+}
+
+QList<CalendarEvent> OrganizerAdapter::items() const
+{
+ return m_items;
+}
diff --git a/rockworkd/platformintegration/ubuntu/organizeradapter.h b/rockworkd/platformintegration/ubuntu/organizeradapter.h
new file mode 100644
index 0000000..2ce8e4d
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/organizeradapter.h
@@ -0,0 +1,33 @@
+#ifndef ORGANIZERADAPTER_H
+#define ORGANIZERADAPTER_H
+
+#include "libpebble/calendarevent.h"
+
+#include <QObject>
+
+#include <QOrganizerManager>
+#include <QOrganizerAbstractRequest>
+#include <QOrganizerEvent>
+
+QTORGANIZER_USE_NAMESPACE
+
+class OrganizerAdapter : public QObject
+{
+ Q_OBJECT
+public:
+ explicit OrganizerAdapter(QObject *parent = 0);
+
+ QList<CalendarEvent> items() const;
+
+public slots:
+ void refresh();
+
+signals:
+ void itemsChanged(const QList<CalendarEvent> &items);
+
+private:
+ QOrganizerManager *m_manager;
+ QList<CalendarEvent> m_items;
+};
+
+#endif // ORGANIZERADAPTER_H
diff --git a/rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp b/rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp
new file mode 100644
index 0000000..b43509e
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * This file is part of sync-monitor.
+ *
+ * sync-monitor is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * contact-service-app is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QTimer>
+
+#include "syncmonitorclient.h"
+
+#define SYNCMONITOR_DBUS_SERVICE_NAME "com.canonical.SyncMonitor"
+#define SYNCMONITOR_DBUS_OBJECT_PATH "/com/canonical/SyncMonitor"
+#define SYNCMONITOR_DBUS_INTERFACE "com.canonical.SyncMonitor"
+
+
+SyncMonitorClient::SyncMonitorClient(QObject *parent)
+ : QObject(parent),
+ m_iface(0)
+{
+ m_iface = new QDBusInterface(SYNCMONITOR_DBUS_SERVICE_NAME,
+ SYNCMONITOR_DBUS_OBJECT_PATH,
+ SYNCMONITOR_DBUS_INTERFACE);
+ if (m_iface->lastError().isValid()) {
+ qWarning() << "Fail to connect with sync monitor:" << m_iface->lastError();
+ return;
+ }
+
+ connect(m_iface, SIGNAL(stateChanged()), SIGNAL(stateChanged()));
+ connect(m_iface, SIGNAL(enabledServicesChanged()), SIGNAL(enabledServicesChanged()));
+ m_iface->call("attach");
+}
+
+SyncMonitorClient::~SyncMonitorClient()
+{
+ if (m_iface) {
+ m_iface->call("detach");
+ delete m_iface;
+ m_iface = 0;
+ }
+}
+
+QString SyncMonitorClient::state() const
+{
+ if (m_iface) {
+ return m_iface->property("state").toString();
+ } else {
+ return "";
+ }
+}
+
+QStringList SyncMonitorClient::enabledServices() const
+{
+ if (m_iface) {
+ return m_iface->property("enabledServices").toStringList();
+ } else {
+ return QStringList();
+ }
+}
+
+/*!
+ Start a new sync for specified services
+*/
+void SyncMonitorClient::sync(const QStringList &services)
+{
+ if (m_iface) {
+ qDebug() << "starting sync!";
+ m_iface->call("sync", services);
+ }
+}
+
+/*!
+ Cancel current sync for specified services
+*/
+void SyncMonitorClient::cancel(const QStringList &services)
+{
+ if (m_iface) {
+ m_iface->call("cancel", services);
+ }
+}
+
+/*!
+ Chek if a specific service is enabled or not
+*/
+bool SyncMonitorClient::serviceIsEnabled(const QString &service)
+{
+ return enabledServices().contains(service);
+}
diff --git a/rockworkd/platformintegration/ubuntu/syncmonitorclient.h b/rockworkd/platformintegration/ubuntu/syncmonitorclient.h
new file mode 100644
index 0000000..1587ba5
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/syncmonitorclient.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * This file is part of sync-monitor.
+ *
+ * sync-monitor is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * contact-service-app is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SYNCMONITOR_QML_H
+#define SYNCMONITOR_QML_H
+
+#include <QObject>
+#include <QDBusInterface>
+
+class SyncMonitorClient : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString state READ state NOTIFY stateChanged)
+ Q_PROPERTY(QStringList enabledServices READ enabledServices NOTIFY enabledServicesChanged)
+
+public:
+ SyncMonitorClient(QObject *parent = 0);
+ ~SyncMonitorClient();
+
+ QString state() const;
+ QStringList enabledServices() const;
+
+Q_SIGNALS:
+ void stateChanged();
+ void enabledServicesChanged();
+
+public Q_SLOTS:
+ void sync(const QStringList &services);
+ void cancel(const QStringList &services);
+ bool serviceIsEnabled(const QString &service);
+
+private:
+ QDBusInterface *m_iface;
+};
+
+#endif
diff --git a/rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp b/rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp
new file mode 100644
index 0000000..7c060b1
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp
@@ -0,0 +1,232 @@
+#include "ubuntuplatform.h"
+
+#include "callchannelobserver.h"
+#include "organizeradapter.h"
+#include "syncmonitorclient.h"
+
+#include <QDBusConnection>
+#include <QDBusConnectionInterface>
+#include <QDebug>
+
+// qmenumodel
+#include "dbus-enums.h"
+#include "liburl-dispatcher-1/url-dispatcher.h"
+
+UbuntuPlatform::UbuntuPlatform(QObject *parent):
+ PlatformInterface(parent),
+ m_volumeActionGroup()
+{
+ // Notifications
+ QDBusConnection::sessionBus().registerObject("/org/freedesktop/Notifications", this, QDBusConnection::ExportAllSlots);
+ m_iface = new QDBusInterface("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus");
+ m_iface->call("AddMatch", "interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'");
+ m_iface->call("AddMatch", "interface='org.freedesktop.Notifications',member='CloseNotification',type='method_call',eavesdrop='true'");
+
+ // Music
+ setupMusicService();
+ m_volumeActionGroup.setBusType(DBusEnums::SessionBus);
+ m_volumeActionGroup.setBusName("com.canonical.indicator.sound");
+ m_volumeActionGroup.setObjectPath("/com/canonical/indicator/sound");
+ m_volumeActionGroup.QDBusObject::connect();
+ connect(&m_volumeActionGroup, &QDBusActionGroup::statusChanged, [this] {
+ if (m_volumeActionGroup.status() == DBusEnums::Connected) {
+ m_volumeAction = m_volumeActionGroup.action("volume");
+ }
+ });
+
+ // Calls
+ m_telepathyMonitor = new TelepathyMonitor(this);
+ connect(m_telepathyMonitor, &TelepathyMonitor::incomingCall, this, &UbuntuPlatform::incomingCall);
+ connect(m_telepathyMonitor, &TelepathyMonitor::callStarted, this, &UbuntuPlatform::callStarted);
+ connect(m_telepathyMonitor, &TelepathyMonitor::callEnded, this, &UbuntuPlatform::callEnded);
+
+ // Organizer
+ m_organizerAdapter = new OrganizerAdapter(this);
+ m_organizerAdapter->refresh();
+ connect(m_organizerAdapter, &OrganizerAdapter::itemsChanged, this, &UbuntuPlatform::organizerItemsChanged);
+ m_syncMonitorClient = new SyncMonitorClient(this);
+ connect(m_syncMonitorClient, &SyncMonitorClient::stateChanged, [this]() { if (m_syncMonitorClient->state() == "idle") m_organizerAdapter->refresh();});
+ m_syncTimer.start(1000 * 60 * 60);
+ connect(&m_syncTimer, &QTimer::timeout, [this]() { m_syncMonitorClient->sync({"calendar"});});
+ m_syncMonitorClient->sync({"calendar"});
+}
+
+QDBusInterface *UbuntuPlatform::interface() const
+{
+ return m_iface;
+}
+
+uint UbuntuPlatform::Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantHash &hints, int expire_timeout)
+{
+ Q_UNUSED(replaces_id)
+ // Lets directly suppress volume change notifications, network password entries and phone call snap decisions here
+ QStringList hiddenNotifications = {"indicator-sound", "indicator-network"};
+ if (!hiddenNotifications.contains(app_name)) {
+ if (hints.contains("x-canonical-secondary-icon") && hints.value("x-canonical-secondary-icon").toString() == "incoming-call") {
+ qDebug() << "Have a phone call notification. Ignoring it..." << app_name << app_icon;
+ } else {
+ qDebug() << "Notification received" << app_name << app_icon << actions << hints << expire_timeout;
+ Notification n(app_name);
+ if (app_name.contains("twitter")) {
+ n.setType(Notification::NotificationTypeTwitter);
+ n.setSourceName("Twitter");
+ } else if (app_name.contains("dekko")) {
+ n.setType(Notification::NotificationTypeEmail);
+ n.setSourceName("EMail");
+ } else if (app_name.toLower().contains("gmail")) {
+ n.setType(Notification::NotificationTypeGMail);
+ n.setSourceName("GMail");
+ } else if (app_name.contains("facebook")) {
+ n.setType(Notification::NotificationTypeFacebook);
+ n.setSourceName("Facebook");
+ } else if (app_name.contains("telegram")) {
+ n.setType(Notification::NotificationTypeTelegram);
+ n.setSourceName("Telegram");
+ } else if (app_name.toLower().contains("hangout")) {
+ n.setType(Notification::NotificationTypeHangout);
+ n.setSourceName("Hangout");
+ } else if (app_name.contains("indicator-datetime")) {
+ n.setType(Notification::NotificationTypeReminder);
+ n.setSourceName("reminders");
+ } else {
+ n.setType(Notification::NotificationTypeGeneric);
+ }
+ n.setSender(summary);
+ n.setBody(body);
+ foreach (const QString &action, actions) {
+ if (action.contains(QRegExp("^[a-z]*://"))) {
+ n.setActToken(action);
+ break;
+ }
+ }
+ qDebug() << "have act token" << n.actToken();
+
+ emit notificationReceived(n);
+ }
+ }
+ // We never return something. We're just snooping in...
+ setDelayedReply(true);
+ return 0;
+}
+
+void UbuntuPlatform::setupMusicService()
+{
+ if (!m_mprisService.isEmpty()) {
+ disconnect(this, SLOT(mediaPropertiesChanged(QString,QVariantMap,QStringList)));
+ }
+
+ QDBusConnectionInterface *iface = QDBusConnection::sessionBus().interface();
+ const QStringList &services = iface->registeredServiceNames();
+ foreach (QString service, services) {
+ if (service.startsWith("org.mpris.MediaPlayer2.")) {
+ qDebug() << "have mpris service" << service;
+ m_mprisService = service;
+ fetchMusicMetadata();
+ QDBusConnection::sessionBus().connect(m_mprisService, "/org/mpris/MediaPlayer2", "", "PropertiesChanged", this, SLOT(mediaPropertiesChanged(QString,QVariantMap,QStringList)));
+ break;
+ }
+ }
+}
+
+void UbuntuPlatform::sendMusicControlCommand(MusicControlButton controlButton)
+{
+ if (m_mprisService.isEmpty()) {
+ setupMusicService();
+ }
+
+ QString method;
+ switch (controlButton) {
+ case MusicControlPlayPause:
+ method = "PlayPause";
+ break;
+ case MusicControlSkipBack:
+ method = "Previous";
+ break;
+ case MusicControlSkipNext:
+ method = "Next";
+ break;
+ default:
+ ;
+ }
+
+ if (!method.isEmpty()) {
+ QDBusMessage call = QDBusMessage::createMethodCall(m_mprisService, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", method);
+ QDBusError err = QDBusConnection::sessionBus().call(call);
+
+ if (err.isValid()) {
+ qWarning() << "Error calling mpris method on" << m_mprisService << ":" << err.message();
+ }
+ return;
+ }
+
+ int volumeDiff = 0;
+ switch (controlButton) {
+ case MusicControlVolumeUp:
+ volumeDiff = 1;
+ break;
+ case MusicControlVolumeDown:
+ volumeDiff = -1;
+ break;
+ default:
+ ;
+ }
+
+ if (m_volumeAction && volumeDiff != 0) {
+ m_volumeAction->activate(volumeDiff);
+ return;
+ }
+}
+
+MusicMetaData UbuntuPlatform::musicMetaData() const
+{
+ return m_musicMetaData;
+}
+
+void UbuntuPlatform::hangupCall(uint cookie)
+{
+ m_telepathyMonitor->hangupCall(cookie);
+}
+
+QList<CalendarEvent> UbuntuPlatform::organizerItems() const
+{
+ return m_organizerAdapter->items();
+}
+
+void UbuntuPlatform::actionTriggered(const QString &actToken)
+{
+ url_dispatch_send(actToken.toStdString().c_str(), [] (const gchar *, gboolean, gpointer) {}, nullptr);
+}
+
+void UbuntuPlatform::fetchMusicMetadata()
+{
+ if (!m_mprisService.isEmpty()) {
+ QDBusMessage call = QDBusMessage::createMethodCall(m_mprisService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
+ call << "org.mpris.MediaPlayer2.Player" << "Metadata";
+ QDBusPendingCall pcall = QDBusConnection::sessionBus().asyncCall(call);
+ QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pcall, this);
+ connect(watcher, &QDBusPendingCallWatcher::finished, this, &UbuntuPlatform::fetchMusicMetadataFinished);
+ }
+}
+
+void UbuntuPlatform::fetchMusicMetadataFinished(QDBusPendingCallWatcher *watcher)
+{
+ watcher->deleteLater();
+ QDBusReply<QDBusVariant> reply = watcher->reply();
+ if (reply.isValid()) {
+ QVariantMap curMetadata = qdbus_cast<QVariantMap>(reply.value().variant().value<QDBusArgument>());
+ m_musicMetaData.artist = curMetadata.value("xesam:artist").toString();
+ m_musicMetaData.album = curMetadata.value("xesam:album").toString();
+ m_musicMetaData.title = curMetadata.value("xesam:title").toString();
+ emit musicMetadataChanged(m_musicMetaData);
+ } else {
+ qWarning() << reply.error().message();
+ }
+}
+
+void UbuntuPlatform::mediaPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps)
+{
+ Q_UNUSED(interface)
+ Q_UNUSED(changedProps)
+ Q_UNUSED(invalidatedProps)
+ fetchMusicMetadata();
+}
diff --git a/rockworkd/platformintegration/ubuntu/ubuntuplatform.h b/rockworkd/platformintegration/ubuntu/ubuntuplatform.h
new file mode 100644
index 0000000..5679a42
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/ubuntuplatform.h
@@ -0,0 +1,62 @@
+#ifndef UBUNTUPLATFORM_H
+#define UBUNTUPLATFORM_H
+
+#include "libpebble/platforminterface.h"
+#include "libpebble/enums.h"
+
+#include <QDBusInterface>
+#include <TelepathyQt/AbstractClientObserver>
+
+#include <qdbusactiongroup.h>
+#include <qstateaction.h>
+
+class QDBusPendingCallWatcher;
+class TelepathyMonitor;
+class OrganizerAdapter;
+class SyncMonitorClient;
+
+class UbuntuPlatform : public PlatformInterface, public QDBusContext
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Notifications")
+ Q_PROPERTY(QDBusInterface* interface READ interface)
+
+
+public:
+ UbuntuPlatform(QObject *parent = 0);
+ QDBusInterface* interface() const;
+
+ void sendMusicControlCommand(MusicControlButton controlButton) override;
+ MusicMetaData musicMetaData() const override;
+
+ void hangupCall(uint cookie) override;
+
+ QList<CalendarEvent> organizerItems() const override;
+
+ void actionTriggered(const QString &actToken) override;
+
+public slots:
+ uint Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantHash &hints, int expire_timeout);
+
+
+private slots:
+ void setupMusicService();
+ void fetchMusicMetadata();
+ void fetchMusicMetadataFinished(QDBusPendingCallWatcher *watcher);
+ void mediaPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps);
+
+private:
+ QDBusInterface *m_iface;
+
+ QString m_mprisService;
+ MusicMetaData m_musicMetaData;
+ QDBusActionGroup m_volumeActionGroup;
+ QStateAction *m_volumeAction = nullptr;
+
+ TelepathyMonitor *m_telepathyMonitor;
+ OrganizerAdapter *m_organizerAdapter;
+ SyncMonitorClient *m_syncMonitorClient;
+ QTimer m_syncTimer;
+};
+
+#endif // UBUNTUPLATFORM_H
diff --git a/rockworkd/rockworkd.pro b/rockworkd/rockworkd.pro
new file mode 100644
index 0000000..e56ced9
--- /dev/null
+++ b/rockworkd/rockworkd.pro
@@ -0,0 +1,146 @@
+QT += core bluetooth dbus network contacts qml location organizer
+QT -= gui
+
+include(../version.pri)
+load(ubuntu-click)
+
+TARGET = rockworkd
+CONFIG += c++11
+#CONFIG -= app_bundle
+
+TEMPLATE = app
+
+#TODO: figure why pkgconfig doesn't work in the click chroot
+#CONFIG += link_pkgconfig
+#PKGCONFIG += url-dispatcher-1
+INCLUDEPATH += /usr/lib/arm-linux-gnueabihf/glib-2.0/include /usr/lib/x86_64-linux-gnu/glib-2.0/include/ /usr/include/glib-2.0/
+LIBS += -lurl-dispatcher
+
+INCLUDEPATH += /usr/include/telepathy-qt5/ /usr/include/qmenumodel/
+LIBS += -lquazip-qt5 -ltelepathy-qt5 -lqmenumodel
+
+SOURCES += main.cpp \
+ libpebble/watchconnection.cpp \
+ libpebble/pebble.cpp \
+ libpebble/watchdatareader.cpp \
+ libpebble/watchdatawriter.cpp \
+ libpebble/notificationendpoint.cpp \
+ libpebble/musicendpoint.cpp \
+ libpebble/phonecallendpoint.cpp \
+ libpebble/musicmetadata.cpp \
+ libpebble/jskit/jskitmanager.cpp \
+ libpebble/jskit/jskitconsole.cpp \
+ libpebble/jskit/jskitgeolocation.cpp \
+ libpebble/jskit/jskitlocalstorage.cpp \
+ libpebble/jskit/jskitpebble.cpp \
+ libpebble/jskit/jskitxmlhttprequest.cpp \
+ libpebble/jskit/jskittimer.cpp \
+ libpebble/jskit/jskitperformance.cpp \
+ libpebble/appinfo.cpp \
+ libpebble/appmanager.cpp \
+ libpebble/appmsgmanager.cpp \
+ libpebble/uploadmanager.cpp \
+ libpebble/bluez/bluezclient.cpp \
+ libpebble/bluez/bluez_agentmanager1.cpp \
+ libpebble/bluez/bluez_adapter1.cpp \
+ libpebble/bluez/bluez_device1.cpp \
+ libpebble/bluez/freedesktop_objectmanager.cpp \
+ libpebble/bluez/freedesktop_properties.cpp \
+ core.cpp \
+ pebblemanager.cpp \
+ dbusinterface.cpp \
+# Platform integration part
+ platformintegration/ubuntu/ubuntuplatform.cpp \
+ platformintegration/ubuntu/callchannelobserver.cpp \
+ libpebble/blobdb.cpp \
+ libpebble/timelineitem.cpp \
+ libpebble/notification.cpp \
+ platformintegration/ubuntu/organizeradapter.cpp \
+ libpebble/calendarevent.cpp \
+ platformintegration/ubuntu/syncmonitorclient.cpp \
+ libpebble/appmetadata.cpp \
+ libpebble/appdownloader.cpp \
+ libpebble/screenshotendpoint.cpp \
+ libpebble/firmwaredownloader.cpp \
+ libpebble/bundle.cpp \
+ libpebble/watchlogendpoint.cpp \
+ libpebble/ziphelper.cpp \
+ libpebble/healthparams.cpp \
+ libpebble/dataloggingendpoint.cpp
+
+HEADERS += \
+ libpebble/watchconnection.h \
+ libpebble/pebble.h \
+ libpebble/watchdatareader.h \
+ libpebble/watchdatawriter.h \
+ libpebble/notificationendpoint.h \
+ libpebble/musicendpoint.h \
+ libpebble/musicmetadata.h \
+ libpebble/phonecallendpoint.h \
+ libpebble/platforminterface.h \
+ libpebble/jskit/jskitmanager.h \
+ libpebble/jskit/jskitconsole.h \
+ libpebble/jskit/jskitgeolocation.h \
+ libpebble/jskit/jskitlocalstorage.h \
+ libpebble/jskit/jskitpebble.h \
+ libpebble/jskit/jskitxmlhttprequest.h \
+ libpebble/jskit/jskittimer.h \
+ libpebble/jskit/jskitperformance.h \
+ libpebble/appinfo.h \
+ libpebble/appmanager.h \
+ libpebble/appmsgmanager.h \
+ libpebble/uploadmanager.h \
+ libpebble/bluez/bluezclient.h \
+ libpebble/bluez/bluez_agentmanager1.h \
+ libpebble/bluez/bluez_adapter1.h \
+ libpebble/bluez/bluez_device1.h \
+ libpebble/bluez/freedesktop_objectmanager.h \
+ libpebble/bluez/freedesktop_properties.h \
+ core.h \
+ pebblemanager.h \
+ dbusinterface.h \
+# Platform integration part
+ platformintegration/ubuntu/ubuntuplatform.h \
+ platformintegration/ubuntu/callchannelobserver.h \
+ libpebble/blobdb.h \
+ libpebble/timelineitem.h \
+ libpebble/notification.h \
+ platformintegration/ubuntu/organizeradapter.h \
+ libpebble/calendarevent.h \
+ platformintegration/ubuntu/syncmonitorclient.h \
+ libpebble/appmetadata.h \
+ libpebble/appdownloader.h \
+ libpebble/enums.h \
+ libpebble/screenshotendpoint.h \
+ libpebble/firmwaredownloader.h \
+ libpebble/bundle.h \
+ libpebble/watchlogendpoint.h \
+ libpebble/ziphelper.h \
+ libpebble/healthparams.h \
+ libpebble/dataloggingendpoint.h
+
+testing: {
+ SOURCES += platformintegration/testing/testingplatform.cpp
+ HEADERS += platformintegration/testing/testingplatform.h
+ RESOURCES += platformintegration/testing/testui.qrc
+ DEFINES += ENABLE_TESTING
+ QT += qml quick
+}
+
+libs.files = /usr/lib/arm-linux-gnueabihf/libQt5Bluetooth.so.5.4.1 \
+ /usr/lib/arm-linux-gnueabihf/libQt5Bluetooth.so.5 \
+ /usr/lib/arm-linux-gnueabihf/libquazip-qt5.so.1.0.0 \
+ /usr/lib/arm-linux-gnueabihf/libquazip-qt5.so.1
+libs.path = $${UBUNTU_CLICK_BINARY_PATH}/..
+INSTALLS += libs
+
+
+# Default rules for deployment.
+target.path = $${UBUNTU_CLICK_BINARY_PATH}
+INSTALLS+=target
+
+QMAKE_POST_LINK = sed -i s/@VERSION@/$$VERSION/g $$OUT_PWD/../manifest.json || exit 0
+#QMAKE_POST_LINK = echo $$OUT_PWD/../manifest.json > /tmp/huhu;
+
+RESOURCES += \
+ libpebble/jskit/jsfiles.qrc