diff options
Diffstat (limited to 'rockworkd/libpebble')
87 files changed, 10373 insertions, 0 deletions
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 ¬ification) +{ + 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 ¬ification); + 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 ¬ification) +{ + 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 ×tamp, 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 ¬ification); + +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 ¬ification) +{ + 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 ¬ification); + + 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 ¬ification); + +// 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 ×tamp, quint16 duration): + TimelineItem(QUuid::createUuid(), type, flags, timestamp, duration) +{ + +} + +TimelineItem::TimelineItem(const QUuid &uuid, TimelineItem::Type type, Flags flags, const QDateTime ×tamp, 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 ×tamp = QDateTime::currentDateTime(), quint16 duration = 0); + TimelineItem(const QUuid &uuid, Type type, Flags flags = FlagNone, const QDateTime ×tamp = 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 |
