summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@cern.ch>2016-02-11 23:55:16 +0100
committerAndrew Branson <andrew.branson@cern.ch>2016-02-11 23:55:16 +0100
commit29aaea2d80a9eb1715b6cddfac2d2aacf76358bd (patch)
tree012795b6bec16c72f38d33cff46324c9a0225868
launchpad ~mzanetti/rockwork/trunk r87
-rw-r--r--README.md34
-rw-r--r--manifest.json.in16
-rw-r--r--po/de.po352
-rw-r--r--po/el.po359
-rw-r--r--po/en_GB.po352
-rw-r--r--po/eu.po365
-rw-r--r--po/fr.po371
-rw-r--r--po/hu.po362
-rw-r--r--po/it.po365
-rw-r--r--po/ms.po367
-rw-r--r--po/pt.po362
-rw-r--r--po/rockwork.mzanetti.pot351
-rw-r--r--po/ru.po352
-rw-r--r--po/template.pot351
-rw-r--r--po/update-rockwork-pot35
-rw-r--r--rockwork.pro30
-rw-r--r--rockwork/AppSettingsPage.qml78
-rw-r--r--rockwork/AppStoreDetailsPage.qml278
-rw-r--r--rockwork/AppStorePage.qml266
-rw-r--r--rockwork/ContentPeerPickerPage.qml41
-rw-r--r--rockwork/DeveloperToolsPage.qml157
-rw-r--r--rockwork/FirmwareUpgradePage.qml58
-rw-r--r--rockwork/HealthSettingsDialog.qml115
-rw-r--r--rockwork/ImportPackagePage.qml32
-rw-r--r--rockwork/InfoPage.qml86
-rw-r--r--rockwork/InstalledAppDelegate.qml88
-rw-r--r--rockwork/InstalledAppsPage.qml201
-rw-r--r--rockwork/Main.qml53
-rw-r--r--rockwork/MainMenuPage.qml317
-rw-r--r--rockwork/NotificationsPage.qml88
-rw-r--r--rockwork/PebbleModels.qml28
-rw-r--r--rockwork/PebblesPage.qml69
-rw-r--r--rockwork/ScreenshotsPage.qml107
-rw-r--r--rockwork/SettingsPage.qml80
-rw-r--r--rockwork/SystemAppIcon.qml67
-rw-r--r--rockwork/applicationsfiltermodel.cpp102
-rw-r--r--rockwork/applicationsfiltermodel.h54
-rw-r--r--rockwork/applicationsmodel.cpp365
-rw-r--r--rockwork/applicationsmodel.h160
-rw-r--r--rockwork/appstoreclient.cpp323
-rw-r--r--rockwork/appstoreclient.h62
-rw-r--r--rockwork/artwork/bianca-black.pngbin0 -> 9165 bytes
-rw-r--r--rockwork/artwork/bianca-silver.pngbin0 -> 10766 bytes
-rw-r--r--rockwork/artwork/black-20mm-hole.pngbin0 -> 44522 bytes
-rw-r--r--rockwork/artwork/bobby-black.pngbin0 -> 16390 bytes
-rw-r--r--rockwork/artwork/bobby-gold.pngbin0 -> 35026 bytes
-rw-r--r--rockwork/artwork/bobby-silver.pngbin0 -> 16599 bytes
-rw-r--r--rockwork/artwork/rockwork.svg275
-rw-r--r--rockwork/artwork/snowy-black.pngbin0 -> 28360 bytes
-rw-r--r--rockwork/artwork/snowy-red.pngbin0 -> 29962 bytes
-rw-r--r--rockwork/artwork/snowy-white.pngbin0 -> 25610 bytes
-rw-r--r--rockwork/artwork/spalding-14mm-black.pngbin0 -> 45124 bytes
-rw-r--r--rockwork/artwork/spalding-14mm-rose-gold.pngbin0 -> 52798 bytes
-rw-r--r--rockwork/artwork/spalding-14mm-silver.pngbin0 -> 36782 bytes
-rw-r--r--rockwork/artwork/spalding-20mm-black.pngbin0 -> 44592 bytes
-rw-r--r--rockwork/artwork/spalding-20mm-silver.pngbin0 -> 38164 bytes
-rw-r--r--rockwork/artwork/tintin-black.pngbin0 -> 5497 bytes
-rw-r--r--rockwork/artwork/tintin-blue.pngbin0 -> 8409 bytes
-rw-r--r--rockwork/artwork/tintin-green.pngbin0 -> 8338 bytes
-rw-r--r--rockwork/artwork/tintin-grey.pngbin0 -> 5493 bytes
-rw-r--r--rockwork/artwork/tintin-orange.pngbin0 -> 6384 bytes
-rw-r--r--rockwork/artwork/tintin-pink.pngbin0 -> 8897 bytes
-rw-r--r--rockwork/artwork/tintin-red.pngbin0 -> 6160 bytes
-rw-r--r--rockwork/artwork/tintin-white.pngbin0 -> 6089 bytes
-rw-r--r--rockwork/main.cpp37
-rw-r--r--rockwork/notificationsourcemodel.cpp117
-rw-r--r--rockwork/notificationsourcemodel.h48
-rw-r--r--rockwork/org.freedesktop.Notifications.xml45
-rw-r--r--rockwork/pebble.cpp432
-rw-r--r--rockwork/pebble.h131
-rw-r--r--rockwork/pebbles.cpp180
-rw-r--r--rockwork/pebbles.h56
-rw-r--r--rockwork/rockwork.apparmor7
-rw-r--r--rockwork/rockwork.desktop8
-rw-r--r--rockwork/rockwork.pro72
-rw-r--r--rockwork/rockwork.qrc48
-rw-r--r--rockwork/rockwork.svg275
-rw-r--r--rockwork/rockwork.url-dispatcher5
-rw-r--r--rockwork/screenshotmodel.cpp71
-rw-r--r--rockwork/screenshotmodel.h38
-rw-r--r--rockwork/servicecontrol.cpp118
-rw-r--r--rockwork/servicecontrol.h38
-rw-r--r--rockwork/snowywhite.pngbin0 -> 14213 bytes
-rw-r--r--rockwork/snowywhite.svg241
-rw-r--r--rockworkd/core.cpp51
-rw-r--r--rockworkd/core.h33
-rw-r--r--rockworkd/dbusinterface.cpp308
-rw-r--r--rockworkd/dbusinterface.h102
-rw-r--r--rockworkd/jsfiles.qrc2
-rw-r--r--rockworkd/libpebble/appdownloader.cpp113
-rw-r--r--rockworkd/libpebble/appdownloader.h32
-rw-r--r--rockworkd/libpebble/appinfo.cpp163
-rw-r--r--rockworkd/libpebble/appinfo.h57
-rw-r--r--rockworkd/libpebble/appmanager.cpp255
-rw-r--r--rockworkd/libpebble/appmanager.h80
-rw-r--r--rockworkd/libpebble/appmetadata.cpp73
-rw-r--r--rockworkd/libpebble/appmetadata.h39
-rw-r--r--rockworkd/libpebble/appmsgmanager.cpp461
-rw-r--r--rockworkd/libpebble/appmsgmanager.h94
-rw-r--r--rockworkd/libpebble/blobdb.cpp584
-rw-r--r--rockworkd/libpebble/blobdb.h108
-rw-r--r--rockworkd/libpebble/bluez/bluez_adapter1.cpp26
-rw-r--r--rockworkd/libpebble/bluez/bluez_adapter1.h66
-rw-r--r--rockworkd/libpebble/bluez/bluez_agentmanager1.cpp26
-rw-r--r--rockworkd/libpebble/bluez/bluez_agentmanager1.h68
-rw-r--r--rockworkd/libpebble/bluez/bluez_device1.cpp26
-rw-r--r--rockworkd/libpebble/bluez/bluez_device1.h85
-rw-r--r--rockworkd/libpebble/bluez/bluez_helper.h30
-rw-r--r--rockworkd/libpebble/bluez/bluezclient.cpp84
-rw-r--r--rockworkd/libpebble/bluez/bluezclient.h51
-rw-r--r--rockworkd/libpebble/bluez/dbus-shared.h36
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp26
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_objectmanager.h58
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_properties.cpp26
-rw-r--r--rockworkd/libpebble/bluez/freedesktop_properties.h71
-rw-r--r--rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml16
-rw-r--r--rockworkd/libpebble/bundle.cpp151
-rw-r--r--rockworkd/libpebble/bundle.h33
-rw-r--r--rockworkd/libpebble/calendarevent.cpp184
-rw-r--r--rockworkd/libpebble/calendarevent.h69
-rw-r--r--rockworkd/libpebble/dataloggingendpoint.cpp44
-rw-r--r--rockworkd/libpebble/dataloggingendpoint.h39
-rw-r--r--rockworkd/libpebble/enums.h95
-rw-r--r--rockworkd/libpebble/firmwaredownloader.cpp246
-rw-r--r--rockworkd/libpebble/firmwaredownloader.h50
-rw-r--r--rockworkd/libpebble/healthparams.cpp93
-rw-r--r--rockworkd/libpebble/healthparams.h52
-rw-r--r--rockworkd/libpebble/jskit/cacheLocalStorage.js11
-rw-r--r--rockworkd/libpebble/jskit/jsfiles.qrc7
-rw-r--r--rockworkd/libpebble/jskit/jskitconsole.cpp29
-rw-r--r--rockworkd/libpebble/jskit/jskitconsole.h20
-rw-r--r--rockworkd/libpebble/jskit/jskitgeolocation.cpp302
-rw-r--r--rockworkd/libpebble/jskit/jskitgeolocation.h66
-rw-r--r--rockworkd/libpebble/jskit/jskitlocalstorage.cpp117
-rw-r--r--rockworkd/libpebble/jskit/jskitlocalstorage.h40
-rw-r--r--rockworkd/libpebble/jskit/jskitmanager.cpp240
-rw-r--r--rockworkd/libpebble/jskit/jskitmanager.h72
-rw-r--r--rockworkd/libpebble/jskit/jskitpebble.cpp355
-rw-r--r--rockworkd/libpebble/jskit/jskitpebble.h47
-rw-r--r--rockworkd/libpebble/jskit/jskitperformance.cpp13
-rw-r--r--rockworkd/libpebble/jskit/jskitperformance.h20
-rw-r--r--rockworkd/libpebble/jskit/jskitsetup.js196
-rw-r--r--rockworkd/libpebble/jskit/jskittimer.cpp77
-rw-r--r--rockworkd/libpebble/jskit/jskittimer.h31
-rw-r--r--rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp318
-rw-r--r--rockworkd/libpebble/jskit/jskitxmlhttprequest.h96
-rw-r--r--rockworkd/libpebble/jskit/typedarray.js1037
-rw-r--r--rockworkd/libpebble/musicendpoint.cpp63
-rw-r--r--rockworkd/libpebble/musicendpoint.h37
-rw-r--r--rockworkd/libpebble/musicmetadata.cpp14
-rw-r--r--rockworkd/libpebble/musicmetadata.h17
-rw-r--r--rockworkd/libpebble/notification.cpp79
-rw-r--r--rockworkd/libpebble/notification.h59
-rw-r--r--rockworkd/libpebble/notificationendpoint.cpp46
-rw-r--r--rockworkd/libpebble/notificationendpoint.h64
-rw-r--r--rockworkd/libpebble/pebble.cpp693
-rw-r--r--rockworkd/libpebble/pebble.h225
-rw-r--r--rockworkd/libpebble/phonecallendpoint.cpp71
-rw-r--r--rockworkd/libpebble/phonecallendpoint.h47
-rw-r--r--rockworkd/libpebble/platforminterface.h46
-rw-r--r--rockworkd/libpebble/screenshotendpoint.cpp131
-rw-r--r--rockworkd/libpebble/screenshotendpoint.h52
-rw-r--r--rockworkd/libpebble/timelineitem.cpp144
-rw-r--r--rockworkd/libpebble/timelineitem.h194
-rw-r--r--rockworkd/libpebble/uploadmanager.cpp331
-rw-r--r--rockworkd/libpebble/uploadmanager.h85
-rw-r--r--rockworkd/libpebble/watchconnection.cpp242
-rw-r--r--rockworkd/libpebble/watchconnection.h154
-rw-r--r--rockworkd/libpebble/watchdatareader.cpp6
-rw-r--r--rockworkd/libpebble/watchdatareader.h146
-rw-r--r--rockworkd/libpebble/watchdatawriter.cpp144
-rw-r--r--rockworkd/libpebble/watchdatawriter.h69
-rw-r--r--rockworkd/libpebble/watchlogendpoint.cpp128
-rw-r--r--rockworkd/libpebble/watchlogendpoint.h76
-rw-r--r--rockworkd/libpebble/ziphelper.cpp91
-rw-r--r--rockworkd/libpebble/ziphelper.h15
-rw-r--r--rockworkd/main.cpp22
-rw-r--r--rockworkd/pebblemanager.cpp95
-rw-r--r--rockworkd/pebblemanager.h35
-rw-r--r--rockworkd/platformintegration/testing/testingplatform.cpp63
-rw-r--r--rockworkd/platformintegration/testing/testingplatform.h31
-rw-r--r--rockworkd/platformintegration/testing/testui.qrc6
-rw-r--r--rockworkd/platformintegration/testing/testui/Main.qml87
-rw-r--r--rockworkd/platformintegration/testing/testui/PebbleController.qml44
-rw-r--r--rockworkd/platformintegration/ubuntu/callchannelobserver.cpp165
-rw-r--r--rockworkd/platformintegration/ubuntu/callchannelobserver.h74
-rw-r--r--rockworkd/platformintegration/ubuntu/organizeradapter.cpp74
-rw-r--r--rockworkd/platformintegration/ubuntu/organizeradapter.h33
-rw-r--r--rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp100
-rw-r--r--rockworkd/platformintegration/ubuntu/syncmonitorclient.h51
-rw-r--r--rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp232
-rw-r--r--rockworkd/platformintegration/ubuntu/ubuntuplatform.h62
-rw-r--r--rockworkd/rockworkd.pro146
-rw-r--r--version.pri2
194 files changed, 22132 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..70a1895
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# Rockwork, the Unofficial Ubuntu Pebble App
+
+An unofficial Pebble App for Ubuntu
+
+## Development
+
+* Install the [Ubuntu SDK](https://developer.ubuntu.com/en/start/ubuntu-sdk/installing-the-sdk/)
+* Setup a [click chroot](https://developer.ubuntu.com/en/apps/sdk/tutorials/click-targets-and-device-kits/) (for armhf compiling)
+* Installing dependencies (in the armhf chroot)
+ * Use the Ubuntu SDKs kit management interface to open up cli access to the chroot
+ * OR access it via command line: `click chroot -a armhf -f ubuntu-sdk-15.04 maint`
+ * Run: `apt-get install qtconnectivity5-dev:armhf libqt5bluetooth5:armhf libtelepathy-qt5-dev:armhf libqmenumodel-dev:armhf libquazip-qt5-dev:armhf liburl-dispatcher1-dev:armhf`
+* Compile Rockwork
+ * Build/Run using the Ubuntu SDK and the desired kit
+ * OR from the cli: `qmake path/to/source && make`
+* Compile Rockwork for Debugging
+ * Edit the build configuration to include CONFIG+=testing in the qmake arguments, then build/run using the Ubuntu SDK and the desired kit
+ * OR from the cli: `qmake path/to/source CONFIG+=testing && make`
+
+## Snooping on Pebble communications
+
+NOTE: This setup requires an Android device
+
+* Install [Wireshark](https://www.wireshark.org/)
+* Launch Wireshark with the [Pebble Dissector](https://github.com/bhdouglass/PebbleDissector)
+ * Run: `wireshark -X lua_script:pebble.lua`
+* Setup [developer options](https://wiki.cyanogenmod.org/w/Doc:_developer_options) on Android
+* In the developer options enable "Enable Bluetooth HCI snoop log"
+* Do something interesting with your Pebble
+* Send the bluetooth log (`/sdcard/btsnoop_hci.log`) to your computer with Wireshark
+* Load btsnoop_hci.log into Wireshark
+ * If Wireshark does not automatically detect the Pebble portions of the log you will need to use the option "Decode As" and make sure options for "BT RFCOMM" are selected
+
+Original guide found here: <http://wordpress.meulenhoff.org/?p=996>
diff --git a/manifest.json.in b/manifest.json.in
new file mode 100644
index 0000000..7d092bf
--- /dev/null
+++ b/manifest.json.in
@@ -0,0 +1,16 @@
+{
+ "name": "rockwork.mzanetti",
+ "description": "The clockwork for your Pebble smartwatch",
+ "architecture": "@CLICK_ARCH@",
+ "title": "RockWork",
+ "hooks": {
+ "rockwork": {
+ "apparmor": "rockwork/rockwork.apparmor",
+ "desktop": "rockwork/rockwork.desktop",
+ "urls": "rockwork/rockwork.url-dispatcher"
+ }
+ },
+ "version": "@VERSION@",
+ "maintainer": "Michael Zanetti <michael_zanetti@gmx.net>",
+ "framework": "ubuntu-sdk-15.04.1"
+}
diff --git a/po/de.po b/po/de.po
new file mode 100644
index 0000000..4ae253d
--- /dev/null
+++ b/po/de.po
@@ -0,0 +1,352 @@
+# German translation for rockwork
+# Copyright (c) 2016 Rosetta Contributors and Canonical Ltd 2016
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-01-12 09:52+0000\n"
+"Last-Translator: Michael Zanetti <michael.zanetti@canonical.com>\n"
+"Language-Team: German <de@li.org>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "App-Einstellungen"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "App-Details"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Installieren"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Installiert"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Beschreibung"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Entwickler"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Version"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Neue App hinzufügen"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Neues Ziffernblatt hinzufügen"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Alle anzeigen"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Bildschirmfotos"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Abbrechen"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Apps & Ziffernblätter"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Apps"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Ziffernblätter"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Starten"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Konfigurieren"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Löschen"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Schließen"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Benachrichtigungen einrichten"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Apps verwalten"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Ziffernblätter verwalten"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Verbunden"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Getrennt"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Benachrichtigungen"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Pebble Uhren verwalten"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Bildschirmfoto-Optionen"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Teilen"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Pebble Bildschirmfoto"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Speichern"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/el.po b/po/el.po
new file mode 100644
index 0000000..8554238
--- /dev/null
+++ b/po/el.po
@@ -0,0 +1,359 @@
+# Greek translation for rockwork
+# Copyright (c) 2016 Rosetta Contributors and Canonical Ltd 2016
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-08 16:59+0000\n"
+"Last-Translator: Aggelos Arnaoutis <angelosarn@hotmail.com>\n"
+"Language-Team: Greek <el@li.org>\n"
+"Language: el\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Ρυθμίσεις εφαρμογής"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Λεπτομέρειες εφαρμογής"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Εγκατάσταση"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "Εγκατάσταση..."
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Εγκατεστημένο"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Περιγραφή"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Προγραμματιστής"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Έκδοση"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Εμφάνιση όλων"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr "Εργαλεία ανάπτυξης"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Στιγμιότυπα οθόνης"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr "Αναφορά προβλήματος"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr "Αποστολή rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr "Αποστολή αρχείων καταγραφής ρολογιού"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Aκύρωση"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Αναβάθμιση υλικολογισμικού"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+"Μια νέα αναβάθμιση υλικολογισμικού είναι διαθέσιμη για το ρολόι Pebble σας."
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Τρέχον εγκατεστημένο υλικολογισμικό: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr "Υποψήφια έκδοση υλικολογισμικού: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Σημειώσεις έκδοσης: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Σημαντικό:"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Αυτή η ενημέρωση θα αναβαθμίση επίσης τα δεδομένα ανάκτησης. Βεβαιωθείτε πως "
+"το ρολόι Pebble σας είναι συνδεδεμένος σε φορτιστή."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr "Ρυθμίσεις υγείας"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr "Η εφαρμογή υγείας είναι ενεργοποιημένη"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr "Γυναίκα"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr "Άνδρας"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr "Ηλικία"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr "Ύψος (cm)"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr "Βάρος"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr "Θέλω να είμαι πιο ενεργός"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr "Θέλω να κοιμηθώ περισσότερο"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr "Εντάξει"
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr "Έκδοση %1"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr "Συντελεστές"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Εφαρμογές"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Διαγραφή"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Κλείσιμο"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr "Εργαλεία για προγραμματιστές"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Διαχείριση εφαρμογών"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr "Ρυθμίσεις"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Συνδεδεμένο"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Αποσυνδεδεμένο"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr "Άνοιγμα ρυθμίσεων συστήματος"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Αρχικοποίηση Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "Αναβάθμιση..."
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Ειδοποιήσεις"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Διαχείριση ρολογιών Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Επιλογές στιγμιοτύπων οθόνης"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Στιγμιότυπο οθόνης Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Αποθήκευση"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr "Μονάδες απόστασης"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr "Μετρικό"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr "Αυτοκρατορικό"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr "Ημερολόγιο"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
+
+#~ msgid "Info"
+#~ msgstr "Πληροφορίες"
diff --git a/po/en_GB.po b/po/en_GB.po
new file mode 100644
index 0000000..b1d9a45
--- /dev/null
+++ b/po/en_GB.po
@@ -0,0 +1,352 @@
+# English (United Kingdom) translation for rockwork
+# Copyright (c) 2016 Rosetta Contributors and Canonical Ltd 2016
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-01-26 18:00+0000\n"
+"Last-Translator: Emanuele Antonio Faraone <emanueleant03@gmail.com>\n"
+"Language-Team: English (United Kingdom) <en_GB@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "App Settings"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "App details"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Install"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Installed"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Description"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Developer"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Version"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Add new watchapp"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Add new watchface"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "See all"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Screenshots"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Cancel"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Apps & Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Apps"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Launch"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Configure"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Delete"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Close"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Manage notifications"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Manage Apps"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Manage Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Connected"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Disconnected"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Notifications"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Manage Pebble Watches"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Screenshot options"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Share"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Pebble screenshot"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Save"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/eu.po b/po/eu.po
new file mode 100644
index 0000000..15aebaf
--- /dev/null
+++ b/po/eu.po
@@ -0,0 +1,365 @@
+# Basque translation for rockwork
+# Copyright (c) 2016 Rosetta Contributors and Canonical Ltd 2016
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-03 22:34+0000\n"
+"Last-Translator: Aitzol Berasategi <aitzol76@gmail.com>\n"
+"Language-Team: Basque <eu@li.org>\n"
+"Language: eu\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Aplikazioaren ezarpenak"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Aplikazioaren xehetasunak"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Instalatu"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "Instalatzen..."
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Instalatuta"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Deskribapena"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Garatzailea"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Bertsioa"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Watchapp berria gehitu"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Watchface berria gehitu"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Guztiak ikusi"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Pantaila-argazkiak"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Utzi"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Firmware eguneraketa"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+"Firmware eguneraketa bat dago eskuragarri zure Pebble smartwatch-earentzat."
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Unean instalaturiko firmwarea: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr "Firmware bertsio hautagaia: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Bertsio oharrak: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Garrantzitsua:"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Eguneraketa honek berreskuratze datuak ere berrituko ditu. Ziurtatu zure "
+"Pebble smartwatch-ea argindar egokitzaile bati konektaturik egotea."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Aplikazio eta Watchface-ak"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Aplikazioak"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Watchface-ak"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Abiarazi"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Konfiguratu"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Ezabatu"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Itxi"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Jakinarazpenak kudeatu"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Aplikazioak kudeatu"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Watchface-ak kudeatu"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Konektatuta"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Deskonektatuta"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+"Zure Pebble smartwatch-ea deskonektaturik dago. Mesedez ziurtatu pizturik "
+"dagoela, irismen muga barruan dagoela eta ongi parekaturik aurkitzen dela "
+"sistemako Bluetooth ezarpenetan."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr "Sistema Ezarpenak Ireki"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+"Zure Pebble smartwatch-ea fabrika moduan aurkitzen da eta beharrezkoa du "
+"berrezartzea."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Pebble-a berrezarri"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "Eguneratzen..."
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Jakinarazpenak"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+"Hemengo aukerak telefonoan jakinarazpenak agertu ahala gehituko dira. "
+"Aukeraturiko jakinarazpenak zure Pebble smartwatchean agertuko dira."
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Pebble ordulariak kudeatu"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+"Oraindik ez dago Pebble smartwatch-erik konfiguraturik. Mesedez konektatu "
+"zure Pebble smartwatch-a Sistema Ezarpenak erabiliz."
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Pantaila-argazkien aukerak"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Partekatu"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Pebble pantaila-argazkia"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Gorde"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/fr.po b/po/fr.po
new file mode 100644
index 0000000..606940a
--- /dev/null
+++ b/po/fr.po
@@ -0,0 +1,371 @@
+# French translation for upebble
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the upebble package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: upebble\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-08 18:15+0000\n"
+"Last-Translator: cm-t arudy <arudy@singularity.fr>\n"
+"Language-Team: French <fr@li.org>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Paramètres de l'application"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Détails de l'application"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Installer"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "Installation..."
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Installées"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Description"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Développeur"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Version"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Ajouter une nouvelle application pour montre"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Ajouter une nouveau cadran"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Tout voir"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr "Outils pour développeurs"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Captures d'écran"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr "Signaler un problème"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr "Envoi de rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr "rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Annuler"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Mise à niveau du micrologiciel"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+"Une nouvelle version du logiciel est disponible pour votre montre connectée "
+"Pebble."
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Micrologiciel actuellement installé : %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr "Version candidate du logiciel : %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Notes de version : %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Important :"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Cette mise à jour mettra également à niveau les données de récupération. "
+"Assurez-vous que votre montre Pebble est connectée à un adaptateur "
+"électrique."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr "Paramètre de Bien-Être"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr "L'application Bien-Être est activé"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr "Femme"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr "Homme"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr "Âge"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr "Taille (cm):"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr "Valider"
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr "Version %1"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr "Contributeurs"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr "Informations légales"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Applications & Cadrans"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Applications"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Cadrans"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Lancer"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Configurer"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Supprimer"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Fermer"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr "Outils de développement"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Gérer les notifications"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Gérer les applications"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Gérer les cadrans"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr "Paramètres"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Connectée"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Déconnectée"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+"Votre montre Pebble est déconnectée. Veuillez vous assurer qu'elle est "
+"allumée, à proximité et appairée correctement dans les Paramètres système / "
+"Bluetooth."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr "Ouvrir les paramètres système"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+"Votre montre connectée Pebble est en mode usine et a besoin d'être "
+"initialisée."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Initialiser Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "Mise à niveau..."
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Notifications"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+"Des entrées seront ajoutées ici lorsque des notifications apparaîtront sur "
+"le téléphone. Les notifications sélectionnées seront affichées sur votre "
+"montre connectée Pebble."
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Gérer les montres Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+"Aucune montre connectée Pebble encore configurée. Veuillez connecter votre "
+"montre Pebble en utilisant les Paramètres système."
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Options de capture d'écran"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Partager"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Capture d'écran de Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Enregistrer"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr "Unités de Distance"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr "Métrique"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr "Impérial"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr "Agenda"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr "Synchroniser l'agenda sur la Timeline."
+
+#~ msgid "Info"
+#~ msgstr "Informations"
diff --git a/po/hu.po b/po/hu.po
new file mode 100644
index 0000000..fc87d6a
--- /dev/null
+++ b/po/hu.po
@@ -0,0 +1,362 @@
+# Hungarian translation for rockwork
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-02 09:30+0000\n"
+"Last-Translator: Richard Somlói <ricsipontaz@gmail.com>\n"
+"Language-Team: Hungarian <hu@li.org>\n"
+"Language: hu\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Alkalmazás beállítások"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Alkalmazás részletei"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Telepítés"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "Telepítés…"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Telepítve"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Leírás"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Fejlesztő"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Verzió"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Új alkalmazás hozzáadása"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Új számlap hozzáadása"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Összes"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Képernyőképek"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Mégse"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Firmware frissítés"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr "Egy új firmware frissítés érhető el a Pebble okosórájához"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Jelenleg telepített firmware: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr "Telepítendő firmware: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Kiadási megjegyzések: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Fontos:"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Ez a frissítés a helyreállítási adatokat is felülírja, ezért mindenképp "
+"csatlakoztassa a töltőhöz a Pebble okosóráját."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Alkalmazások és számlapok"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Alkalmazások"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Számlapok"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Indítás"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Beállítások"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Törlés"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Bezárás"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Értesítések kezelése"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Alkalmazások kezelése"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Számlapok kezelése"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Kapcsolódva"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Leválasztva"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+"A Pebble okosórája nem található. Győződjön meg róla, hogy be van kapcsolva, "
+"hatótávon belül van és megfelelően párosította a Rendszerbeállítások "
+"Bluetooth menüpontja alatt."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr "Rendszerbeállítások megnyitása"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr "Az Pebble okosórája gyári állapotban van és inicializálni kell."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Pebble inicializálása"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "Frissítés…"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Értesítések"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+"Az itt megadott bejegyzések esetén az értesítések nem csak a telefonján, "
+"hanem a Pebble okosóráján is meg fognak jelenni."
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Pebble órák kezelése"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+"Nincs bekonfigurált Pebble okosóra. Csatlakoztassa az órát a "
+"Rendszerbeállítások alatt."
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Képernyőkép-beállítások"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Megosztás"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Pebble képernyőkép"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Mentés"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/it.po b/po/it.po
new file mode 100644
index 0000000..e43daa1
--- /dev/null
+++ b/po/it.po
@@ -0,0 +1,365 @@
+# Italian translation for upebble
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the upebble package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: upebble\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-08 13:28+0000\n"
+"Last-Translator: Emanuele Antonio Faraone <emanueleant03@gmail.com>\n"
+"Language-Team: Italian <it@li.org>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Impostazioni dell' applicazione"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Dettagli sull'app"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Installa"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "Installazione in corso..."
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Installato"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Descrizione"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Sviluppatore"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Versione"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Aggiungi una nuova watchapp"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Aggiungi una nuova watchapp"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Mostra tutto"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr "Strumenti per lo sviluppo"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Screenshots"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr "riporta un problema"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr "preparando il log dei pacchetti..."
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr "Invia rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr "rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr "Invia Log del Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Cancella"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Aggiornamento Firmware"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr "Un nuovo aggiornamento del firmware è disponibile per il tuo Pebble."
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Versione del firmware installata prima: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr "Versione candidata all'installazione: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Note di release: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Importanza:"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Questo aggiornamento aggiornerá il sistema. Connetti il tuo Smartwhatch "
+"Pebble alla presa di corrente."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr "Impostazioni Salute"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr "App Salute è attiva"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr "Femmina"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr "Maschio"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr "Età"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr "Altezza (cm)"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr "Peso"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr "Vorrei Essere più attivo"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr "Vorrei dormire di più"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr "OK"
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr "Versione %1"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr "Collaboratori"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr "Note legali"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr "Quest' applicazione non è affiliata con Pebble Technology Corp."
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr "Pebble è un Marchio Registrato di Pebble Technology Corp."
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Apps & Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "App"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Avvia"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Configura"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Elimina"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Chiudi"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr "Strumenti per sviluppatori"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Gestisci le notifiche"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Gestisci le applicazioni"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Gestisci le Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr "Impostazioni"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Connesso"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Disconnesso"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+"Il tuo Pebble è disconnesso. Controlla che sia acceso,che non sia lontano e "
+"controlla se è accoppiato con questo smartphone nelle impostazioni del "
+"Bluetooth ."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr "Impostazioni aperte"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr "Il tuo Pebble è in modalitá di fabbrica e sta per essere inizializzato"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Inizializza il Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "Aggiornamento in corso..."
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Notifiche"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+"Scrivi quali notifiche dovranno apparire nel telefono. Quelle selezionate "
+"appariranno nel Pebble ."
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Gestisci le Whatchfaces Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+"Nessuno Smartwhatch Pebble connesso. Connetti il tuo Pebble usando le "
+"impostazioni."
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Opzioni sugli Screenshot"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Condividi"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Screenshot Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Salva"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr "Unità di distanza"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr "Metrica"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr "Imperiale"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr "Calendario"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr "Sincronizza con la timeline del calendario"
+
+#~ msgid "Info"
+#~ msgstr "Informazioni"
diff --git a/po/ms.po b/po/ms.po
new file mode 100644
index 0000000..e038c5a
--- /dev/null
+++ b/po/ms.po
@@ -0,0 +1,367 @@
+# Malay translation for rockwork
+# Copyright (c) 2016 Rosetta Contributors and Canonical Ltd 2016
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-08 19:12+0000\n"
+"Last-Translator: Adrian Chiang <Unknown>\n"
+"Language-Team: Malay <ms@li.org>\n"
+"Language: ms\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Tetapan Aplikasi"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Butiran app"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Memasang"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "Memasangkan..."
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Dipasang"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Penghuraian"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Pemaju"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Versi"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Tambahkan aplikasi jam baru"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Tambahkan muka jam baru"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Lihat semua"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr "Peralatan pemaju"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Tangkapan skrin"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr "Laporkan masalah"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr "Menyediakan pakej log..."
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr "Hantarkan rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr "rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr "Hantarkan log jam"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Batal"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Memperbarui perisian tetap"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr "Perisian tetap yang baru tersedia untuk jam Pebble anda."
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Perisian tetap sedia ada: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr "Versi perisian tetap yang baru: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Nota Keluaran: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Penting:"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Pembaruan ini akan juga memperbarui data pemulihan jam anda. Pastikan jam "
+"Pebble anda bersambung dengan kabel pengecasan semasa diperbarui."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr "Tetapan kesihatan"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr "Tetapan kesihatan dibolehkan"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr "Perempuan"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr "Lelaki"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr "Umur"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr "Ketinggian (sm)"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr "Berat badan"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr "Saya mahu menjadi lebih aktif"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr "Saya mahu lebih tidur"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr "Baik"
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr "Versi %1"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr "Penyumbang-penyumbang"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr "Perundangan"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+"Aplikasi in tidak berkenaan dengan atau disahkan oleh Pebble Technology Corp."
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr "Pebble adalah tanda dagang Pebble Technology Corp."
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Aplikasi & Muka jam"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Aplikasi"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Muka jam"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Lancarkan"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Konfigurasi"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Padam"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Tutup"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr "Peralatan pemaju"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Mengurus notifikasi"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Mengurus aplikasi"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Mengurus muka jam"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr "Tetapan"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Bersambung"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Terputus"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+"Jam Pebble anda terputus. Sila pastikan ia dihidupkan, berada dekat dengan "
+"dan dipasangkan dengan betul melalui tetapan sistem Bluetooth."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr "Buka Tetapan Sistem"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+"Jam Pebble anda berada dalam keadaan mod kilang dan perlu dimulakan semula."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Memulakan Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "Dinaiktarafkan..."
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Notifikasi-notifikasi"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+"Penyertaan aplikasi di sini akan ditambah apabila notifikasi perisai muncul "
+"di telefon. Notifikasi aplikasi yang dipilih akan dipaparkan pada jam Pebble "
+"anda."
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Menguruskan Jam Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+"Tiada jam Pebble dikonfigur lagi. Sila sambungkan jam Pebble anda menerusi "
+"Sistem Tetapan."
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Pilihan tangkapan skrin"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Kongsi"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Tangkapan skrin Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Simpan"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr "Unit Jarak"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr "Metrik"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr "Imperial"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr "Kalendar"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr "Segerakkan kalendar dengan Timeline"
+
+#~ msgid "Info"
+#~ msgstr "Maklumat"
diff --git a/po/pt.po b/po/pt.po
new file mode 100644
index 0000000..f18423a
--- /dev/null
+++ b/po/pt.po
@@ -0,0 +1,362 @@
+# Portuguese translation for upebble
+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
+# This file is distributed under the same license as the upebble package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: upebble\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-02-08 15:44+0000\n"
+"Last-Translator: Vitor Loureiro <Unknown>\n"
+"Language-Team: Portuguese <pt@li.org>\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Definições"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Detalhes"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Instalar"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr "A instalar..."
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Instalado"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Descrição"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Programador"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Versão"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Adicionar nova watchapp"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Adicionar nova watchface"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "Ver todos"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr "Ferramentas de Desenvolvimento"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Capturas de ecrã"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr "Reportar problema"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr "Enviar rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr "rockworkd.log"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr "Enviar logs do relógio"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr "Atualização do firmware"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+#, fuzzy
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+"Uma nova actualização do firmware está disponível para o seu Pebble "
+"smartwatch"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr "Firmware instalado atualmente: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr "Notas da versão: %1"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr "Importante:"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+"Esta atualização também irá atualizar os dados de recuperação . Verifique se "
+"o seu smartwarch Pebble está conectado a um adaptador de energia."
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr "Feminino"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr "Masculino"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr "Idade"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr "Altura (cm)"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr "Peso"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr "Eu quero ser mais ativo"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr "Eu quero dormir mais"
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr "OK"
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr "Versão %1"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr "Contribuidores"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr "Legal"
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr "Pebble é uma marca comercial da Pebble Technology Corp."
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Apps e Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Apps"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Iniciar"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Configurar"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Eliminar"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Fechar"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr "Ferramentas do programador"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Gerir notificações"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Gerir apps"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Gerir watchfaces"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr "Configurações"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Ligado"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Desconectado"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+"O seu smartwatch Pebble está no modo de fábrica e precisa ser inicializado."
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr "Inicializar o Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr "A actualizar..."
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Notificações"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+"As entradas aqui vão ser adicionadas como notificações no telefone. As "
+"notificações seleccionadas serão exibidas no seu smartwatch Pebble."
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Gerir relógios Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+"Não existem smartwatches Pebble configurados ainda. Por favor, ligue o seu "
+"smartwatch Pebble através de Configurações do sistema."
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Opções de capturas do ecrã"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Partilhar"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Capturas de ecrã Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Guardar"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr "Unidades de distância"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr "Métrica"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr "Imperial"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/rockwork.mzanetti.pot b/po/rockwork.mzanetti.pot
new file mode 100644
index 0000000..0f5cbce
--- /dev/null
+++ b/po/rockwork.mzanetti.pot
@@ -0,0 +1,351 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/ru.po b/po/ru.po
new file mode 100644
index 0000000..b5622cc
--- /dev/null
+++ b/po/ru.po
@@ -0,0 +1,352 @@
+# Russian translation for rockwork
+# Copyright (c) 2016 Rosetta Contributors and Canonical Ltd 2016
+# This file is distributed under the same license as the rockwork package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: rockwork\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: 2016-01-23 13:33+0000\n"
+"Last-Translator: Eugene Marshal <Unknown>\n"
+"Language-Team: Russian <ru@li.org>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2016-02-09 06:16+0000\n"
+"X-Generator: Launchpad (build 17908)\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr "Настройки"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr "Сведения"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr "Установить"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr "Установлено"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr "Описание"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr "Разработчик"
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr "Версия"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr "Добавить приложение"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr "Добавить циферблат"
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr "См. все"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr "Снимки экрана"
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr "Отмена"
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr "Приложения и циферблаты"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr "Приложения"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr "Циферблаты"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr "Запустить"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr "Настроить"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr "Удалить"
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr "Закрыть"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr "Управление уведомлениями"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr "Управление приложениями"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr "Управление циферблатами"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr "Подключено"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr "Отключено"
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr "Уведомления"
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr "Управление часами Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr "Параметры снимка экрана"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr "Поделиться"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr "Снимок экрана Pebble"
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr "Сохранить"
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/template.pot b/po/template.pot
new file mode 100644
index 0000000..0f5cbce
--- /dev/null
+++ b/po/template.pot
@@ -0,0 +1,351 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-02-09 23:31+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /home/micha/Develop/upebble/rockwork/AppSettingsPage.qml:13
+msgid "App Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:9
+msgid "App details"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Install"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installing..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:57
+msgid "Installed"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:235
+msgid "Description"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:258
+msgid "Developer"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStoreDetailsPage.qml:266
+msgid "Version"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchapp"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:8
+msgid "Add new watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/AppStorePage.qml:125
+msgid "See all"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:9
+msgid "Developer Tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:28
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:11
+msgid "Screenshots"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:35
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:102
+msgid "Report problem"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:42
+msgid "Install app or watchface from file"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:109
+msgid "Preparing logs package..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:120
+msgid "pebble.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:127
+msgid "Send rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:132
+msgid "rockworkd.log"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:137
+msgid "Send watch logs"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/DeveloperToolsPage.qml:146
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:110
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:99
+msgid "Cancel"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:6
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:84
+msgid "Firmware upgrade"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:16
+msgid "A new firmware upgrade is available for your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:23
+#, qt-format
+msgid "Currently installed firmware: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:29
+#, qt-format
+msgid "Candidate firmware version: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:35
+#, qt-format
+msgid "Release Notes: %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid "Important:"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/FirmwareUpgradePage.qml:41
+msgid ""
+"This update will also upgrade recovery data. Make sure your Pebble "
+"smartwarch is connected to a power adapter."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:9
+msgid "Health settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:17
+msgid "Health app enabled"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Female"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:28
+msgid "Male"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:34
+msgid "Age"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:47
+msgid "Height (cm)"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:60
+msgid "Weight"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:73
+msgid "I want to be more active"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:84
+msgid "I want to sleep more"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/HealthSettingsDialog.qml:95
+msgid "OK"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ImportPackagePage.qml:7
+msgid "Import watchapp or watchface"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:31
+#, qt-format
+msgid "Version %1"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:40
+msgid "Contributors"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:52
+msgid "Legal"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:74
+msgid ""
+"This application is neither affiliated with nor endorsed by Pebble "
+"Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InfoPage.qml:79
+msgid "Pebble is a trademark of Pebble Technology Corp."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps & Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Apps"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:9
+msgid "Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:167
+msgid "Launch"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:176
+msgid "Configure"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:186
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:91
+msgid "Delete"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/InstalledAppsPage.qml:196
+msgid "Close"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:15
+msgid "About"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:22
+msgid "Developer tools"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:52
+msgid "Manage notifications"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:59
+msgid "Manage Apps"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:67
+msgid "Manage Watchfaces"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:75
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:8
+msgid "Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Connected"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:190
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:36
+msgid "Disconnected"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:203
+msgid ""
+"Your Pebble smartwatch is disconnected. Please make sure it is powered on, "
+"within range and it is paired properly in the Bluetooth System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:213
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:64
+msgid "Open System Settings"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:221
+msgid "Your Pebble smartwatch is in factory mode and needs to be initialized."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:230
+msgid "Initialize Pebble"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/MainMenuPage.qml:262
+msgid "Upgrading..."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:8
+msgid "Notifications"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/NotificationsPage.qml:29
+msgid ""
+"Entries here will be added as notifications appear on the phone. Selected "
+"notifications will be shown on your Pebble smartwatch."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:6
+msgid "Manage Pebble Watches"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/PebblesPage.qml:57
+msgid ""
+"No Pebble smartwatches configured yet. Please connect your Pebble smartwatch "
+"using System Settings."
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:69
+msgid "Screenshot options"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:74
+msgid "Share"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:77
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:85
+msgid "Pebble screenshot"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/ScreenshotsPage.qml:82
+msgid "Save"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:19
+msgid "Distance Units"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:35
+msgid "Metric"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:48
+msgid "Imperial"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:55
+msgid "Calendar"
+msgstr ""
+
+#: /home/micha/Develop/upebble/rockwork/SettingsPage.qml:62
+msgid "Sync calendar to timeline"
+msgstr ""
diff --git a/po/update-rockwork-pot b/po/update-rockwork-pot
new file mode 100644
index 0000000..7596ca7
--- /dev/null
+++ b/po/update-rockwork-pot
@@ -0,0 +1,35 @@
+#!/bin/sh
+set -e
+cd $(dirname $0)
+PO_DIR=$PWD
+
+PROGNAME=$(basename $0)
+
+# Create a list of files to scan
+GETTEXT_FILES=$(mktemp --tmpdir unity8.lst.XXXXX)
+trap 'rm -f "$GETTEXT_FILES"' EXIT
+cd ..
+find \( -name '*.h' -o -name '*.cpp' -o -name '*.qml' -o -name '*.js' \) \
+ -a ! \( -path './debian/*' -o -path './builddir/*' -o -path './build/*' -o -path './tests/*' -o -path './.bzr/*' \) | sort \
+> $GETTEXT_FILES
+
+# Generate pot from our list
+xgettext \
+ --output $PO_DIR/rockwork.mzanetti.pot \
+ --from-code=UTF-8 \
+ --files-from $GETTEXT_FILES \
+ --qt --c++ \
+ --add-comments=Translators \
+ --keyword=tr \
+ --keyword=tr:1,2 \
+ --keyword=dtr:2 \
+ --keyword=dtr:2,3 \
+ --keyword=ctr:1c,2 \
+ --keyword=dctr:2c,3 \
+ --keyword=tag \
+ --keyword=tag:1c,2 \
+ --keyword=dgettext_datetime \
+ --package-name="rockwork" \
+ --from-code="UTF-8"
+
+echo "$PROGNAME: $PO_DIR/rockwork.pot updated"
diff --git a/rockwork.pro b/rockwork.pro
new file mode 100644
index 0000000..f2aceee
--- /dev/null
+++ b/rockwork.pro
@@ -0,0 +1,30 @@
+# This is the basic qmake template for the Ubuntu-SDK
+# it handles creation and installation of the manifest
+# file and takes care of subprojects
+TEMPLATE = subdirs
+
+#load Ubuntu specific features
+load(ubuntu-click)
+
+SUBDIRS += rockworkd rockwork
+
+# specify the manifest file, this file is required for click
+# packaging and for the IDE to create runconfigurations
+UBUNTU_MANIFEST_FILE=manifest.json.in
+
+# specify translation domain, this must be equal with the
+# app name in the manifest file
+UBUNTU_TRANSLATION_DOMAIN="rockwork.mzanetti"
+
+# specify the source files that should be included into
+# the translation file, from those files a translation
+# template is created in po/template.pot, to create a
+# translation copy the template to e.g. de.po and edit the sources
+UBUNTU_TRANSLATION_SOURCES+= \
+ $$files(*.qml,true) \
+ $$files(*.js,true) \
+ $$files(*.desktop,true)
+
+# specifies all translations files and makes sure they are
+# compiled and installed into the right place in the click package
+UBUNTU_PO_FILES+=$$files(po/*.po)
diff --git a/rockwork/AppSettingsPage.qml b/rockwork/AppSettingsPage.qml
new file mode 100644
index 0000000..d8d865b
--- /dev/null
+++ b/rockwork/AppSettingsPage.qml
@@ -0,0 +1,78 @@
+import QtQuick 2.4
+import Ubuntu.Web 0.2
+import Ubuntu.Components 1.3
+import com.canonical.Oxide 1.0 as Oxide
+
+Page {
+ id: settings
+
+ property string uuid;
+ property string url;
+ property var pebble;
+
+ title: i18n.tr("App Settings")
+
+ WebContext {
+ id: webcontext
+ userAgent: "Mozilla/5.0 (Linux; Android 5.0; Nexus 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.102 Mobile Safari/537.36 Ubuntu Touch (RockWork)"
+ }
+
+ WebView {
+ id: webview
+ anchors {
+ fill: parent
+ bottom: parent.bottom
+ }
+ width: parent.width
+ height: parent.height
+
+ context: webcontext
+ url: settings.url
+ preferences.localStorageEnabled: true
+ preferences.appCacheEnabled: true
+ preferences.javascriptCanAccessClipboard: true
+
+ function navigationRequestedDelegate(request) {
+ //The pebblejs:// protocol is handeled by the urihandler, as it appears we can't intercept it
+
+ var url = request.url.toString();
+ console.log(url, url.substring(0, 16));
+ if (url.substring(0, 16) == 'pebblejs://close') {
+ pebble.configurationClosed(settings.uuid, url);
+ request.action = Oxide.NavigationRequest.ActionReject;
+ pageStack.pop();
+ }
+ }
+
+ Component.onCompleted: {
+ preferences.localStorageEnabled = true;
+ }
+ }
+
+ ProgressBar {
+ height: units.dp(3)
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+
+ showProgressPercentage: false
+ value: (webview.loadProgress / 100)
+ visible: (webview.loading && !webview.lastLoadStopped)
+ }
+
+ Connections {
+ target: UriHandler
+ onOpened: {
+ if (uris && uris[0] && uris[0].length) {
+ var url = uris[0];
+
+ if (url.substring(0, 16) == 'pebblejs://close') {
+ pebble.configurationClosed(settings.uuid, url);
+ pageStack.pop();
+ }
+ }
+ }
+ }
+}
diff --git a/rockwork/AppStoreDetailsPage.qml b/rockwork/AppStoreDetailsPage.qml
new file mode 100644
index 0000000..696e3c6
--- /dev/null
+++ b/rockwork/AppStoreDetailsPage.qml
@@ -0,0 +1,278 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3
+import QtGraphicalEffects 1.0
+
+Page {
+ id: root
+ title: i18n.tr("App details")
+
+ property var pebble: null
+ property var app: null
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: units.gu(1)
+
+ Item {
+ Layout.fillWidth: true
+ height: headerColumn.height + units.gu(1)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+ height: headerColumn.height
+
+ UbuntuShape {
+ id: iconShape
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+
+ source: Image {
+ height: iconShape.height
+ width: iconShape.width
+ source: root.app.icon
+ }
+ }
+
+ ColumnLayout {
+ id: headerColumn
+ Layout.fillWidth: true
+ Label {
+ text: root.app.name
+ fontSize: "large"
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ }
+ Label {
+ text: root.app.vendor
+ Layout.fillWidth: true
+ }
+ }
+
+ Button {
+ id: installButton
+ text: enabled ? i18n.tr("Install") : (installing && !installed ? i18n.tr("Installing...") : i18n.tr("Installed"))
+ color: UbuntuColors.green
+ enabled: !installed && !installing
+ property bool installing: false
+ property bool installed: root.pebble.installedApps.contains(root.app.storeId) || root.pebble.installedWatchfaces.contains(root.app.storeId)
+ Connections {
+ target: root.pebble.installedApps
+ onChanged: {
+ installButton.installed = root.pebble.installedApps.contains(root.app.storeId) || root.pebble.installedWatchfaces.contains(root.app.storeId)
+ }
+ }
+
+ Connections {
+ target: root.pebble.installedWatchfaces
+ onChanged: {
+ installButton.installed = root.pebble.installedApps.contains(root.app.storeId) || root.pebble.installedWatchfaces.contains(root.app.storeId)
+ }
+ }
+
+ onClicked: {
+ root.pebble.installApp(root.app.storeId)
+ installButton.installing = true
+ }
+ }
+ }
+ }
+
+ Flickable {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ contentHeight: contentColumn.height
+ bottomMargin: units.gu(1)
+ clip: true
+
+ Column {
+ id: contentColumn
+ width: parent.width
+ height: childrenRect.height
+
+ Image {
+ width: parent.width
+ // ss.w : ss.h = w : h
+ height: sourceSize.height * width / sourceSize.width
+ fillMode: Image.PreserveAspectFit
+ source: root.app.headerImage
+ }
+
+ RowLayout {
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+ height: units.gu(6)
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Row {
+ anchors.centerIn: parent
+ spacing: units.gu(1)
+ Icon {
+ name: "like"
+ height: parent.height
+ width: height
+ }
+ Label {
+ text: root.app.hearts
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.preferredHeight: parent.height - units.gu(2)
+ Layout.preferredWidth: units.dp(1)
+ color: UbuntuColors.lightGrey
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Row {
+ anchors.centerIn: parent
+ spacing: units.gu(1)
+ Icon {
+ name: root.app.isWatchFace ? "clock-app-symbolic" : "stock_application"
+ height: parent.height
+ width: height
+ }
+ Label {
+ text: root.app.isWatchFace ? "Watchface" : "Watchapp"
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors { left: parent.left; right: parent.right; margins: units.gu(1) }
+ spacing: units.gu(1)
+
+ PebbleModels {
+ id: modelModel
+ }
+
+
+ Item {
+ id: screenshotsItem
+ Layout.preferredHeight: units.gu(20)
+ Layout.fillWidth: true
+
+ property bool isRound: modelModel.get(root.pebble.model).shape === "round"
+
+ ListView {
+ id: screenshotsListView
+ anchors.centerIn: parent
+ width: parent.width
+ height: screenshotsItem.isRound ? units.gu(10) : units.gu(9.5)
+ orientation: ListView.Horizontal
+ spacing: units.gu(1)
+ snapMode: ListView.SnapToItem
+ preferredHighlightBegin: (screenshotsListView.width - height * .95) / 2
+ preferredHighlightEnd: (screenshotsListView.width + height * .95) / 2
+ highlightRangeMode: ListView.StrictlyEnforceRange
+
+ model: root.app.screenshotImages
+ delegate: AnimatedImage {
+ height: screenshotsListView.height
+ width: height * 0.95
+ fillMode: Image.PreserveAspectFit
+ source: modelData
+ }
+ }
+ Image {
+ id: watchImage
+ // ssw : ssh = w : h
+ height: parent.height
+ width: height * sourceSize.width / sourceSize.height
+ fillMode: Image.PreserveAspectFit
+ anchors.centerIn: parent
+ source: modelModel.get(root.pebble.model).image
+ Rectangle {
+ anchors.centerIn: parent
+ height: units.gu(10)
+ width: height
+ color: "black"
+ radius: screenshotsItem.isRound ? height / 2 : 0
+ }
+ }
+
+ OpacityMask {
+ anchors.fill: screenshotsListView
+ source: screenshotsListView
+ maskSource: maskRect
+ }
+
+ Rectangle {
+ id: maskRect
+ anchors.fill: screenshotsListView
+ color: "transparent"
+ visible: false
+
+ Rectangle {
+ color: "blue"
+ anchors.centerIn: parent
+ height: screenshotsListView.height
+ width: screenshotsItem.isRound ? height : height * 0.9
+ radius: screenshotsItem.isRound ? height / 2 : units.gu(.5)
+// anchors.fill: watchImage
+// anchors.margins: units.gu(5)
+// radius: modelModel.get(root.pebble.model).shape === "rectangle" ? units.gu(.5) : height / 2
+// visible: false
+ }
+ }
+
+ }
+
+ Label {
+ Layout.fillWidth: true
+ font.bold: true
+ text: i18n.tr("Description")
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: units.dp(1)
+ color: UbuntuColors.lightGrey
+ }
+
+ Label {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ wrapMode: Text.WordWrap
+ text: root.app.description
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ columns: 2
+ columnSpacing: units.gu(1)
+ rowSpacing: units.gu(1)
+ Label {
+ text: i18n.tr("Developer")
+ font.bold: true
+ }
+ Label {
+ text: root.app.vendor
+ Layout.fillWidth: true
+ }
+ Label {
+ text: i18n.tr("Version")
+ font.bold: true
+ }
+ Label {
+ text: root.app.version
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/rockwork/AppStorePage.qml b/rockwork/AppStorePage.qml
new file mode 100644
index 0000000..bb8712b
--- /dev/null
+++ b/rockwork/AppStorePage.qml
@@ -0,0 +1,266 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import QtQuick.Layouts 1.1
+import RockWork 1.0
+
+Page {
+ id: root
+ title: showWatchApps ? i18n.tr("Add new watchapp") : i18n.tr("Add new watchface")
+
+ property var pebble: null
+ property bool showWatchApps: false
+ property bool showWatchFaces: false
+
+ property string link: ""
+
+ function fetchHome() {
+ if (showWatchApps) {
+ client.fetchHome(AppStoreClient.TypeWatchapp)
+ } else {
+ client.fetchHome(AppStoreClient.TypeWatchface)
+ }
+ }
+
+ head {
+ actions: [
+ Action {
+ iconName: "search"
+ onTriggered: {
+ if (searchField.shown) {
+ searchField.shown = false;
+ root.fetchHome();
+ } else {
+ searchField.shown = true;
+ }
+ }
+ }
+ ]
+ }
+
+ Component.onCompleted: {
+ if (root.link) {
+ client.fetchLink(link)
+ } else {
+ root.fetchHome()
+ }
+ }
+
+ AppStoreClient {
+ id: client
+ hardwarePlatform: pebble.hardwarePlatform
+ }
+
+ Item {
+ id: searchField
+ anchors { left: parent.left; right: parent.right; top: parent.top }
+ anchors.topMargin: shown ? 0 : -height
+ Behavior on anchors.topMargin { UbuntuNumberAnimation {} }
+ opacity: shown ? 1 : 0
+ Behavior on opacity { UbuntuNumberAnimation {} }
+ height: units.gu(6)
+
+ property bool shown: false
+ onShownChanged: {
+ if (shown) {
+ searchTextField.focus = true;
+ }
+ }
+
+ TextField {
+ id: searchTextField
+ anchors.centerIn: parent
+ width: parent.width - units.gu(2)
+ onDisplayTextChanged: {
+ searchTimer.restart()
+ }
+
+ Timer {
+ id: searchTimer
+ interval: 300
+ onTriggered: {
+ client.search(searchTextField.displayText, root.showWatchApps ? AppStoreClient.TypeWatchapp : AppStoreClient.TypeWatchface);
+ }
+ }
+ }
+ }
+
+ Item {
+ anchors { left: parent.left; top: searchField.bottom; right: parent.right; bottom: parent.bottom }
+ ListView {
+ anchors.fill: parent
+ model: ApplicationsFilterModel {
+ id: appsFilterModel
+ model: client.model
+ }
+ clip: true
+ section.property: "groupId"
+ section.labelPositioning: ViewSection.CurrentLabelAtStart |
+ ViewSection.InlineLabels
+ section.delegate: ListItem {
+ height: section ? label.implicitHeight + units.gu(3) : 0
+
+ Rectangle {
+ anchors.fill: parent
+ color: "white"
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ Label {
+ id: label
+ text: client.model.groupName(section)
+ fontSize: "large"
+// font.weight: Font.DemiBold
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ AbstractButton {
+ Layout.fillHeight: true
+ implicitWidth: seeAllLabel.implicitWidth + height
+ Row {
+ anchors.verticalCenter: parent.verticalCenter
+ Label {
+ id: seeAllLabel
+ text: i18n.tr("See all")
+ }
+ Icon {
+ implicitHeight: parent.height
+ implicitWidth: height
+ name: "go-next"
+ }
+ }
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("AppStorePage.qml"), {pebble: root.pebble, link: client.model.groupLink(section), title: client.model.groupName(section)});
+ }
+ }
+ }
+ }
+
+ footer: Item {
+ height: client.model.links.length > 0 ? units.gu(6) : 0
+ width: parent.width
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: units.gu(1)
+ }
+ spacing: units.gu(1)
+
+ Repeater {
+ model: client.model.links
+ Button {
+ text: client.model.linkName(client.model.links[index])
+ onClicked: client.fetchLink(client.model.links[index]);
+ color: UbuntuColors.orange
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+ delegate: ListItem {
+ height: delegateColumn.height + units.gu(2)
+
+ RowLayout {
+ id: delegateRow
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ AnimatedImage {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ source: model.icon
+ asynchronous: true
+// sourceSize.width: width
+// sourceSize.height: height
+ }
+
+ ColumnLayout {
+ id: delegateColumn
+ Layout.fillWidth: true;
+ Layout.fillHeight: true;
+ Label {
+ Layout.fillWidth: true
+ text: model.name
+ font.weight: Font.DemiBold
+ elide: Text.ElideRight
+ }
+ Label {
+ Layout.fillWidth: true
+ text: model.category
+ }
+ RowLayout {
+ Icon {
+ name: "like"
+ Layout.preferredHeight: parent.height
+ Layout.preferredWidth: height
+ implicitHeight: parent.height
+ }
+ Label {
+ Layout.fillWidth: true
+ text: model.hearts
+ }
+ Icon {
+ id: tickIcon
+ name: "tick"
+ implicitHeight: parent.height
+ Layout.preferredWidth: height
+ visible: root.pebble.installedApps.contains(model.storeId) || root.pebble.installedWatchfaces.contains(model.storeId)
+ Connections {
+ target: root.pebble.installedApps
+ onChanged: {
+ tickIcon.visible = root.pebble.installedApps.contains(model.storeId) || root.pebble.installedWatchfaces.contains(model.storeId)
+ }
+ }
+
+ Connections {
+ target: root.pebble.installedWatchfaces
+ onChanged: {
+ tickIcon.visible = root.pebble.installedApps.contains(model.storeId) || root.pebble.installedWatchfaces.contains(model.storeId)
+ }
+ }
+
+ }
+ }
+ }
+
+ }
+
+ onClicked: {
+ client.fetchAppDetails(model.storeId);
+ pageStack.push(Qt.resolvedUrl("AppStoreDetailsPage.qml"), {app: appsFilterModel.get(index), pebble: root.pebble})
+ }
+ }
+ }
+
+// RowLayout {
+// id: buttonRow
+// anchors { left: parent.left; bottom: parent.bottom; right: parent.right; margins: units.gu(1) }
+// spacing: units.gu(1)
+// Button {
+// text: i18n.tr("Previous")
+// Layout.fillWidth: true
+// enabled: client.offset > 0
+// onClicked: {
+// client.previous()
+// }
+// }
+// Button {
+// text: i18n.tr("Next")
+// Layout.fillWidth: true
+// onClicked: {
+// client.next()
+// }
+// }
+// }
+ }
+
+ ActivityIndicator {
+ anchors.centerIn: parent
+ running: client.busy
+ }
+}
+
diff --git a/rockwork/ContentPeerPickerPage.qml b/rockwork/ContentPeerPickerPage.qml
new file mode 100644
index 0000000..7ee9702
--- /dev/null
+++ b/rockwork/ContentPeerPickerPage.qml
@@ -0,0 +1,41 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import Ubuntu.Content 1.3
+import RockWork 1.0
+
+Page {
+ id: pickerPage
+ head {
+ locked: true
+ visible: false
+ }
+
+ property alias contentType: contentPeerPicker.contentType
+ property string itemName
+ property alias handler: contentPeerPicker.handler
+ property string filename
+
+ Component {
+ id: exportItemComponent
+ ContentItem {
+ name: pickerPage.itemName
+ }
+ }
+ ContentPeerPicker {
+ id: contentPeerPicker
+ anchors.fill: parent
+
+ onCancelPressed: pageStack.pop()
+
+ onPeerSelected: {
+ var transfer = peer.request();
+ var items = [];
+ var item = exportItemComponent.createObject();
+ item.url = "file://" + pickerPage.filename;
+ items.push(item)
+ transfer.items = items;
+ transfer.state = ContentTransfer.Charged;
+ pageStack.pop();
+ }
+ }
+}
diff --git a/rockwork/DeveloperToolsPage.qml b/rockwork/DeveloperToolsPage.qml
new file mode 100644
index 0000000..2f77254
--- /dev/null
+++ b/rockwork/DeveloperToolsPage.qml
@@ -0,0 +1,157 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Content 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Developer Tools")
+
+ property var pebble: null
+
+ //Creating the menu list this way to allow the text field to be translatable (http://askubuntu.com/a/476331)
+ ListModel {
+ id: devMenuModel
+ dynamicRoles: true
+ }
+
+ Component.onCompleted: {
+ populateDevMenu();
+ }
+
+ function populateDevMenu() {
+ devMenuModel.clear();
+
+ devMenuModel.append({
+ icon: "camera-app-symbolic",
+ text: i18n.tr("Screenshots"),
+ page: "ScreenshotsPage.qml",
+ dialog: "",
+ color: "gold"
+ });
+ devMenuModel.append({
+ icon: "dialog-warning-symbolic",
+ text: i18n.tr("Report problem"),
+ page: "",
+ dialog: sendLogsComponent,
+ color: UbuntuColors.red
+ });
+ devMenuModel.append({
+ icon: "stock_application",
+ text: i18n.tr("Install app or watchface from file"),
+ page: "ImportPackagePage.qml",
+ dialog: null,
+ color: UbuntuColors.blue
+ });
+
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ Repeater {
+ id: menuRepeater
+ model: devMenuModel
+ delegate: ListItem {
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+
+ UbuntuShape {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ backgroundColor: model.color
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ name: model.icon
+ color: "white"
+ }
+ }
+
+
+ Label {
+ text: model.text
+ Layout.fillWidth: true
+ }
+ }
+
+ onClicked: {
+ if (model.page) {
+ pageStack.push(Qt.resolvedUrl(model.page), {pebble: root.pebble})
+ }
+ if (model.dialog) {
+ PopupUtils.open(model.dialog)
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+ }
+
+ Component {
+ id: sendLogsComponent
+ Dialog {
+ id: sendLogsDialog
+ title: i18n.tr("Report problem")
+ ActivityIndicator {
+ id: busyIndicator
+ visible: false
+ running: visible
+ }
+ Label {
+ text: i18n.tr("Preparing logs package...")
+ visible: busyIndicator.visible
+ horizontalAlignment: Text.AlignHCenter
+ fontSize: "large"
+ }
+
+ Connections {
+ target: root.pebble
+ onLogsDumped: {
+ if (success) {
+ var filename = "/tmp/pebble.log"
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("pebble.log"),handler: ContentHandler.Share, contentType: ContentType.All, filename: filename })
+ }
+ PopupUtils.close(sendLogsDialog)
+ }
+ }
+
+ Button {
+ text: i18n.tr("Send rockworkd.log")
+ color: UbuntuColors.blue
+ visible: !busyIndicator.visible
+ onClicked: {
+ var filename = homePath + "/.cache/upstart/rockworkd.log"
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("rockworkd.log"),handler: ContentHandler.Share, contentType: ContentType.All, filename: filename })
+ PopupUtils.close(sendLogsDialog)
+ }
+ }
+ Button {
+ text: i18n.tr("Send watch logs")
+ color: UbuntuColors.blue
+ visible: !busyIndicator.visible
+ onClicked: {
+ busyIndicator.visible = true
+ root.pebble.dumpLogs("/tmp/pebble.log")
+ }
+ }
+ Button {
+ text: i18n.tr("Cancel")
+ color: UbuntuColors.red
+ visible: !busyIndicator.visible
+ onClicked: {
+ PopupUtils.close(sendLogsDialog)
+ }
+ }
+ }
+ }
+
+}
+
diff --git a/rockwork/FirmwareUpgradePage.qml b/rockwork/FirmwareUpgradePage.qml
new file mode 100644
index 0000000..3281a12
--- /dev/null
+++ b/rockwork/FirmwareUpgradePage.qml
@@ -0,0 +1,58 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Firmware upgrade")
+
+ property var pebble: null
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(2)
+
+ Label {
+ text: i18n.tr("A new firmware upgrade is available for your Pebble smartwatch.")
+ fontSize: "large"
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: i18n.tr("Currently installed firmware: %1").arg("<b>" + root.pebble.softwareVersion + "</b>")
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: i18n.tr("Candidate firmware version: %1").arg("<b>" + root.pebble.candidateVersion + "</b>")
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: "<b>" + i18n.tr("Release Notes: %1").arg("</b><br>" + root.pebble.firmwareReleaseNotes)
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: "<b>" + i18n.tr("Important:") + "</b> " + i18n.tr("This update will also upgrade recovery data. Make sure your Pebble smartwarch is connected to a power adapter.")
+ width: parent.width
+ wrapMode: Text.WordWrap
+ visible: root.pebble.candidateVersion.indexOf("mig") > 0
+ }
+
+ Button {
+ text: "Upgrade now"
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: UbuntuColors.blue
+ onClicked: {
+ root.pebble.performFirmwareUpgrade();
+ pageStack.pop();
+ }
+ }
+ }
+}
+
diff --git a/rockwork/HealthSettingsDialog.qml b/rockwork/HealthSettingsDialog.qml
new file mode 100644
index 0000000..94e5d22
--- /dev/null
+++ b/rockwork/HealthSettingsDialog.qml
@@ -0,0 +1,115 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Components.ListItems 1.3
+
+Dialog {
+ id: root
+ title: i18n.tr("Health settings")
+
+ property var healthParams: null
+
+ signal accepted();
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Health app enabled")
+ Layout.fillWidth: true
+ }
+ Switch {
+ id: enabledSwitch
+ checked: healthParams["enabled"]
+ }
+ }
+
+ ItemSelector {
+ id: genderSelector
+ model: [i18n.tr("Female"), i18n.tr("Male")]
+ selectedIndex: root.healthParams["gender"] === "female" ? 0 : 1
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Age")
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: ageField
+ inputMethodHints: Qt.ImhDigitsOnly
+ text: healthParams["age"]
+ Layout.preferredWidth: units.gu(10)
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Height (cm)")
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: heightField
+ inputMethodHints: Qt.ImhDigitsOnly
+ text: healthParams["height"]
+ Layout.preferredWidth: units.gu(10)
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("Weight")
+ Layout.fillWidth: true
+ }
+ TextField {
+ id: weightField
+ inputMethodHints: Qt.ImhDigitsOnly
+ text: healthParams["weight"]
+ Layout.preferredWidth: units.gu(10)
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("I want to be more active")
+ Layout.fillWidth: true
+ }
+ Switch {
+ id: moreActiveSwitch
+ checked: healthParams["moreActive"]
+ }
+ }
+
+ RowLayout {
+ Label {
+ text: i18n.tr("I want to sleep more")
+ Layout.fillWidth: true
+ }
+ Switch {
+ id: sleepMoreSwitch
+ checked: healthParams["sleepMore"]
+ }
+ }
+
+
+ Button {
+ text: i18n.tr("OK")
+ color: UbuntuColors.green
+ onClicked: {
+ root.healthParams["enabled"] = enabledSwitch.checked;
+ root.healthParams["gender"] = genderSelector.selectedIndex == 0 ? "female" : "male"
+ root.healthParams["age"] = ageField.text;
+ root.healthParams["height"] = heightField.text;
+ root.healthParams["weight"] = weightField.text;
+ root.healthParams["moreActive"] = moreActiveSwitch.checked;
+ root.healthParams["sleepMore"] = sleepMoreSwitch.checked;
+ root.accepted();
+ PopupUtils.close(root);
+ }
+ }
+ Button {
+ text: i18n.tr("Cancel")
+ color: UbuntuColors.red
+ onClicked: PopupUtils.close(root)
+ }
+}
+
diff --git a/rockwork/ImportPackagePage.qml b/rockwork/ImportPackagePage.qml
new file mode 100644
index 0000000..4f86f78
--- /dev/null
+++ b/rockwork/ImportPackagePage.qml
@@ -0,0 +1,32 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import Ubuntu.Content 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Import watchapp or watchface")
+
+ property var pebble: null
+
+ ContentPeerPicker {
+ anchors.fill: parent
+ handler: ContentHandler.Source
+ contentType: ContentType.All
+ showTitle: false
+
+ onPeerSelected: {
+ var transfer = peer.request();
+
+ transfer.stateChanged.connect(function() {
+ if (transfer.state == ContentTransfer.Charged) {
+ for (var i = 0; i < transfer.items.length; i++) {
+ print("sideloading package", transfer.items[i].url)
+ root.pebble.sideloadApp(transfer.items[i].url)
+ }
+ pageStack.pop();
+ }
+ })
+ }
+ }
+}
+
diff --git a/rockwork/InfoPage.qml b/rockwork/InfoPage.qml
new file mode 100644
index 0000000..3eec387
--- /dev/null
+++ b/rockwork/InfoPage.qml
@@ -0,0 +1,86 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3
+
+Page {
+ title: "About RockWork"
+
+ Flickable {
+ anchors.fill: parent
+ contentHeight: contentColumn.height + units.gu(4)
+
+ ColumnLayout {
+ id: contentColumn
+ anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(2) }
+ spacing: units.gu(2)
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: units.gu(2)
+ UbuntuShape {
+ source: Image {
+ anchors.fill: parent
+ source: "artwork/rockwork.svg"
+ }
+ height: units.gu(6)
+ width: height
+ }
+
+ Label {
+ text: i18n.tr("Version %1").arg(version)
+ Layout.fillWidth: true
+ fontSize: "large"
+ }
+ }
+
+ ThinDivider {}
+
+ Label {
+ text: i18n.tr("Contributors")
+ Layout.fillWidth: true
+ font.bold: true
+ }
+ Label {
+ text: "Michael Zanetti<br>Brian Douglas<br>Katharine Berry"
+ Layout.fillWidth: true
+ }
+
+ ThinDivider {}
+
+ Label {
+ text: i18n.tr("Legal")
+ Layout.fillWidth: true
+ font.bold: true
+ }
+
+ Label {
+ text: "This program is free software: you can redistribute it and/or modify" +
+ "it under the terms of the GNU General Public License as published by" +
+ "the Free Software Foundation, version 3 of the License.<br>" +
+
+ "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.<br>" +
+
+ "You should have received a copy of the GNU General Public License" +
+ "along with this program. If not, see <http://www.gnu.org/licenses/>."
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ text: i18n.tr("This application is neither affiliated with nor endorsed by Pebble Technology Corp.")
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ Label {
+ text: i18n.tr("Pebble is a trademark of Pebble Technology Corp.")
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+}
+
diff --git a/rockwork/InstalledAppDelegate.qml b/rockwork/InstalledAppDelegate.qml
new file mode 100644
index 0000000..89f6ba8
--- /dev/null
+++ b/rockwork/InstalledAppDelegate.qml
@@ -0,0 +1,88 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import RockWork 1.0
+
+ListItem {
+ id: root
+
+ property string uuid: ""
+ property string name: ""
+ property string iconSource: ""
+ property string vendor: ""
+ property bool hasSettings: false
+ property alias hasGrip: grip.visible
+ property bool isSystemApp: false
+
+ signal deleteApp();
+ signal configureApp();
+
+ leadingActions: ListItemActions {
+ actions: [
+ Action {
+ visible: !root.isSystemApp
+ iconName: "delete"
+ onTriggered: {
+ root.deleteApp();
+ }
+ }
+ ]
+ }
+
+ trailingActions: ListItemActions {
+ actions: [
+ Action {
+ visible: root.hasSettings
+ iconName: "settings"
+ onTriggered: {
+ print("settings triggered")
+ root.configureApp();
+ }
+ }
+ ]
+ }
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: units.gu(1)
+ }
+ spacing: units.gu(1)
+
+ SystemAppIcon {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ isSystemApp: root.isSystemApp
+ uuid: root.uuid
+ iconSource: root.iconSource
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Label {
+ text: root.name
+ Layout.fillWidth: true
+ }
+
+ Label {
+ text: root.vendor
+ Layout.fillWidth: true
+ fontSize: "small"
+ }
+ }
+
+ Item {
+ id: grip
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ opacity: (root.contentMoving || root.swiped || root.dragging) ? 0 : 1
+ Behavior on opacity { UbuntuNumberAnimation {} }
+ Icon {
+ width: units.gu(3)
+ height: width
+ anchors.centerIn: parent
+ name: "grip-large"
+ }
+ }
+ }
+}
diff --git a/rockwork/InstalledAppsPage.qml b/rockwork/InstalledAppsPage.qml
new file mode 100644
index 0000000..a18cd3f
--- /dev/null
+++ b/rockwork/InstalledAppsPage.qml
@@ -0,0 +1,201 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import RockWork 1.0
+
+Page {
+ id: root
+ title: showWatchApps ? (showWatchFaces ? i18n.tr("Apps & Watchfaces") : i18n.tr("Apps")) : i18n.tr("Watchfaces")
+
+ property var pebble: null
+ property bool showWatchApps: false
+ property bool showWatchFaces: false
+
+ head {
+ actions: [
+ Action {
+ iconName: "add"
+ onTriggered: pageStack.push(Qt.resolvedUrl("AppStorePage.qml"), {pebble: root.pebble, showWatchApps: root.showWatchApps, showWatchFaces: root.showWatchFaces})
+ }
+ ]
+ }
+
+ function configureApp(uuid) {
+ // The health app is special :/
+ if (uuid == "{36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c}") {
+ var popup = PopupUtils.open(Qt.resolvedUrl("HealthSettingsDialog.qml"), root, {healthParams: pebble.healthParams});
+ popup.accepted.connect(function() {
+ pebble.healthParams = popup.healthParams
+ })
+ } else {
+ pebble.requestConfigurationURL(uuid);
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ ListView {
+ id: listView
+ anchors.fill: parent
+ model: root.showWatchApps ? root.pebble.installedApps : root.pebble.installedWatchfaces
+ clip: true
+ property real realContentY: contentY + originY
+
+ delegate: InstalledAppDelegate {
+ id: delegate
+ uuid: model.uuid
+ name: model.name
+ iconSource: model.icon
+ vendor: model.vendor
+ visible: dndArea.draggedIndex !== index
+ hasGrip: index > 0
+ isSystemApp: model.isSystemApp
+ hasSettings: model.hasSettings
+
+ onDeleteApp: {
+ pebble.removeApp(model.uuid)
+ }
+ onConfigureApp: {
+ root.configureApp(model.uuid)
+ }
+ onClicked: {
+ PopupUtils.open(dialogComponent, root, {app: listView.model.get(index)})
+ }
+ }
+ }
+ MouseArea {
+ id: dndArea
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ }
+ drag.axis: Drag.YAxis
+ propagateComposedEvents: true
+ width: units.gu(5)
+
+ property int startY: 0
+ property int draggedIndex: -1
+
+
+ onPressAndHold: {
+ startY = mouseY;
+ draggedIndex = Math.floor((listView.realContentY + mouseY) / fakeDragItem.height)
+ if (draggedIndex == 0) {
+ print("cannot drag settings app");
+ return;
+ }
+
+ var draggedItem = listView.model.get(draggedIndex);
+ fakeDragItem.uuid = draggedItem.uuid;
+ fakeDragItem.name = draggedItem.name;
+ fakeDragItem.vendor = draggedItem.vendor;
+ fakeDragItem.iconSource = draggedItem.icon;
+ fakeDragItem.isSystemApp = draggedItem.isSystemApp;
+ fakeDragItem.y = (fakeDragItem.height * draggedIndex) - listView.realContentY
+ drag.target = fakeDragItem;
+ }
+
+ onMouseYChanged: {
+ var newIndex = Math.floor((listView.realContentY + mouseY) / fakeDragItem.height)
+
+ if (newIndex > draggedIndex) {
+ newIndex = draggedIndex + 1;
+ } else if (newIndex < draggedIndex) {
+ newIndex = draggedIndex - 1;
+ } else {
+ return;
+ }
+
+ if (newIndex >= 1 && newIndex < listView.count) {
+ listView.model.move(draggedIndex, newIndex);
+ draggedIndex = newIndex;
+ }
+ }
+
+ onReleased: {
+ if (draggedIndex > -1) {
+ listView.model.commitMove();
+ draggedIndex = -1;
+ drag.target = null;
+ }
+ }
+ }
+ }
+
+
+
+ InstalledAppDelegate {
+ id: fakeDragItem
+ visible: dndArea.draggedIndex != -1
+
+ }
+
+ Component {
+ id: dialogComponent
+ Dialog {
+ id: dialog
+ property var app: null
+
+ RowLayout {
+ SystemAppIcon {
+ height: titleCol.height
+ width: height
+ isSystemApp: app.isSystemApp
+ uuid: app.uuid
+ iconSource: app.icon
+ }
+
+ ColumnLayout {
+ id: titleCol
+ Layout.fillWidth: true
+
+ Label {
+ Layout.fillWidth: true
+ text: app.name
+ fontSize: "large"
+ }
+ Label {
+ Layout.fillWidth: true
+ text: app.vendor
+ }
+ }
+ }
+
+ Button {
+ text: i18n.tr("Launch")
+ color: UbuntuColors.green
+ onClicked: {
+ pebble.launchApp(app.uuid);
+ PopupUtils.close(dialog);
+ }
+ }
+
+ Button {
+ text: i18n.tr("Configure")
+ color: UbuntuColors.blue
+ visible: app.hasSettings
+ onClicked: {
+ root.configureApp(app.uuid);
+ PopupUtils.close(dialog);
+ }
+ }
+
+ Button {
+ text: i18n.tr("Delete")
+ color: UbuntuColors.red
+ visible: !app.isSystemApp
+ onClicked: {
+ pebble.removeApp(app.uuid);
+ PopupUtils.close(dialog);
+ }
+ }
+
+ Button {
+ text: i18n.tr("Close")
+ onClicked: PopupUtils.close(dialog)
+ }
+ }
+ }
+}
diff --git a/rockwork/Main.qml b/rockwork/Main.qml
new file mode 100644
index 0000000..2bdece3
--- /dev/null
+++ b/rockwork/Main.qml
@@ -0,0 +1,53 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import RockWork 1.0
+
+/*!
+ \brief MainView with a Label and Button elements.
+*/
+
+MainView {
+ applicationName: "rockwork.mzanetti"
+
+ width: units.gu(40)
+ height: units.gu(70)
+
+ ServiceController {
+ id: serviceController
+ serviceName: "rockworkd"
+ Component.onCompleted: {
+ if (!serviceController.serviceFileInstalled) {
+ print("Service file not installed. Installing now.")
+ serviceController.installServiceFile();
+ }
+ if (!serviceController.serviceRunning) {
+ print("Service not running. Starting now.")
+ serviceController.startService();
+ }
+ if (pebbles.version !== version) {
+ print("Service file version (", version, ") is not equal running service version (", pebbles.version, "). Restarting service.")
+ serviceController.restartService();
+ }
+ }
+ }
+
+ Pebbles {
+ id: pebbles
+ onCountChanged: loadStack()
+ }
+
+ function loadStack() {
+ pageStack.clear()
+ if (pebbles.count == 1) {
+ pageStack.push(Qt.resolvedUrl("MainMenuPage.qml"), {pebble: pebbles.get(0)})
+ } else {
+ pageStack.push(Qt.resolvedUrl("PebblesPage.qml"))
+ }
+ }
+
+ PageStack {
+ id: pageStack
+ Component.onCompleted: loadStack();
+ }
+}
diff --git a/rockwork/MainMenuPage.qml b/rockwork/MainMenuPage.qml
new file mode 100644
index 0000000..32c7b96
--- /dev/null
+++ b/rockwork/MainMenuPage.qml
@@ -0,0 +1,317 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+
+Page {
+ id: root
+ title: pebble.name
+
+ property var pebble: null
+
+ head {
+ actions: [
+ Action {
+ iconName: "info"
+ text: i18n.tr("About")
+ onTriggered: {
+ pageStack.push(Qt.resolvedUrl("InfoPage.qml"))
+ }
+ },
+ Action {
+ iconName: "ubuntu-sdk-symbolic"
+ text: i18n.tr("Developer tools")
+ onTriggered: {
+ pageStack.push(Qt.resolvedUrl("DeveloperToolsPage.qml"), {pebble: root.pebble})
+ }
+ }
+ ]
+ }
+
+ //Creating the menu list this way to allow the text field to be translatable (http://askubuntu.com/a/476331)
+ ListModel {
+ id: mainMenuModel
+ dynamicRoles: true
+ }
+
+ Component.onCompleted: {
+ populateMainMenu();
+ }
+
+ Connections {
+ target: root.pebble
+ onFirmwareUpgradeAvailableChanged: {
+ populateMainMenu();
+ }
+ }
+
+ function populateMainMenu() {
+ mainMenuModel.clear();
+
+ mainMenuModel.append({
+ icon: "stock_notification",
+ text: i18n.tr("Manage notifications"),
+ page: "NotificationsPage.qml",
+ color: "blue"
+ });
+
+ mainMenuModel.append({
+ icon: "stock_application",
+ text: i18n.tr("Manage Apps"),
+ page: "InstalledAppsPage.qml",
+ showWatchApps: true,
+ color: UbuntuColors.green
+ });
+
+ mainMenuModel.append({
+ icon: "clock-app-symbolic",
+ text: i18n.tr("Manage Watchfaces"),
+ page: "InstalledAppsPage.qml",
+ showWatchFaces: true,
+ color: "black"
+ });
+
+ mainMenuModel.append({
+ icon: "settings",
+ text: i18n.tr("Settings"),
+ page: "SettingsPage.qml",
+ showWatchFaces: true,
+ color: "gold"
+ });
+
+ if (root.pebble.firmwareUpgradeAvailable) {
+ mainMenuModel.append({
+ icon: "preferences-system-updates-symbolic",
+ text: i18n.tr("Firmware upgrade"),
+ page: "FirmwareUpgradePage.qml",
+ color: "red"
+ });
+ }
+
+ }
+
+ PebbleModels {
+ id: modelModel
+ }
+
+ GridLayout {
+ anchors.fill: parent
+ columns: parent.width > parent.height ? 2 : 1
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.maximumHeight: units.gu(30)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ Item {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.minimumWidth: watchImage.width
+ Image {
+ id: watchImage
+ width: implicitWidth * height / implicitHeight
+ height: parent.height
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ source: modelModel.get(root.pebble.model).image
+ fillMode: Image.PreserveAspectFit
+
+ Item {
+ id: watchFace
+ height: parent.height * (modelModel.get(root.pebble.model - 1).shape === "rectangle" ? .5 : .515)
+ width: height * (modelModel.get(root.pebble.model - 1).shape === "rectangle" ? .85 : 1)
+ anchors.centerIn: parent
+ anchors.horizontalCenterOffset: units.dp(1)
+ anchors.verticalCenterOffset: units.dp(modelModel.get(root.pebble.model - 1).shape === "rectangle" ? 0 : 1)
+
+ Image {
+ id: image
+ anchors.fill: parent
+ source: "file://" + root.pebble.screenshots.latestScreenshot
+ visible: false
+ }
+
+ Component.onCompleted: {
+ if (!root.pebble.screenshots.latestScreenshot) {
+ root.pebble.requestScreenshot();
+ }
+ }
+
+ Rectangle {
+ id: textItem
+ anchors.fill: parent
+ layer.enabled: true
+ radius: modelModel.get(root.pebble.model - 1).shape === "rectangle" ? units.gu(.5) : height / 2
+ // This item should be used as the 'mask'
+ layer.samplerName: "maskSource"
+ layer.effect: ShaderEffect {
+ property var colorSource: image;
+ fragmentShader: "
+ uniform lowp sampler2D colorSource;
+ uniform lowp sampler2D maskSource;
+ uniform lowp float qt_Opacity;
+ varying highp vec2 qt_TexCoord0;
+ void main() {
+ gl_FragColor =
+ texture2D(colorSource, qt_TexCoord0)
+ * texture2D(maskSource, qt_TexCoord0).a
+ * qt_Opacity;
+ }
+ "
+ }
+ }
+ }
+ }
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: units.gu(2)
+ Rectangle {
+ height: units.gu(10)
+ width: height
+ radius: height / 2
+ color: root.pebble.connected ? UbuntuColors.green : UbuntuColors.red
+
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(2)
+ color: "white"
+ name: root.pebble.connected ? "tick" : "dialog-error-symbolic"
+ }
+ }
+
+ Label {
+ text: root.pebble.connected ? i18n.tr("Connected") : i18n.tr("Disconnected")
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+
+ Column {
+ Layout.fillWidth: true
+ Layout.preferredHeight: childrenRect.height
+ spacing: menuRepeater.count > 0 ? 0 : units.gu(2)
+ Label {
+ text: i18n.tr("Your Pebble smartwatch is disconnected. Please make sure it is powered on, within range and it is paired properly in the Bluetooth System Settings.")
+ width: parent.width - units.gu(4)
+ anchors.horizontalCenter: parent.horizontalCenter
+ wrapMode: Text.WordWrap
+ visible: !root.pebble.connected
+ fontSize: "large"
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Button {
+ text: i18n.tr("Open System Settings")
+ visible: !root.pebble.connected
+ onClicked: Qt.openUrlExternally("settings://system/bluetooth")
+ color: UbuntuColors.orange
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Label {
+ text: i18n.tr("Your Pebble smartwatch is in factory mode and needs to be initialized.")
+ width: parent.width - units.gu(4)
+ anchors.horizontalCenter: parent.horizontalCenter
+ wrapMode: Text.WordWrap
+ visible: root.pebble.connected && root.pebble.recovery && !root.pebble.upgradingFirmware
+ fontSize: "large"
+ horizontalAlignment: Text.AlignHCenter
+ }
+ Button {
+ text: i18n.tr("Initialize Pebble")
+ onClicked: root.pebble.performFirmwareUpgrade();
+ visible: root.pebble.connected && root.pebble.recovery && !root.pebble.upgradingFirmware
+ color: UbuntuColors.orange
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Rectangle {
+ id: upgradeIcon
+ height: units.gu(10)
+ width: height
+ radius: width / 2
+ color: UbuntuColors.orange
+ anchors.horizontalCenter: parent.horizontalCenter
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ name: "preferences-system-updates-symbolic"
+ color: "white"
+ }
+
+ RotationAnimation on rotation {
+ duration: 2000
+ loops: Animation.Infinite
+ from: 0
+ to: 360
+ running: upgradeIcon.visible
+ }
+ visible: root.pebble.connected && root.pebble.upgradingFirmware
+ }
+
+ Label {
+ text: i18n.tr("Upgrading...")
+ fontSize: "large"
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: root.pebble.connected && root.pebble.upgradingFirmware
+ }
+
+ Repeater {
+ id: menuRepeater
+ model: root.pebble.connected && !root.pebble.recovery && !root.pebble.upgradingFirmware ? mainMenuModel : null
+ delegate: ListItem {
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+
+ UbuntuShape {
+ Layout.fillHeight: true
+ Layout.preferredWidth: height
+ backgroundColor: model.color
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ name: model.icon
+ color: "white"
+ }
+ }
+
+
+ Label {
+ text: model.text
+ Layout.fillWidth: true
+ }
+ }
+
+ onClicked: {
+ var options = {};
+ options["pebble"] = root.pebble
+ var modelItem = mainMenuModel.get(index)
+ options["showWatchApps"] = modelItem.showWatchApps
+ options["showWatchFaces"] = modelItem.showWatchFaces
+ pageStack.push(Qt.resolvedUrl(model.page), options)
+ }
+ }
+ }
+ }
+ }
+
+ Connections {
+ target: pebble
+ onOpenURL: {
+ if (url) {
+ pageStack.push(Qt.resolvedUrl("AppSettingsPage.qml"), {uuid: uuid, url: url, pebble: pebble})
+ }
+ }
+ }
+}
diff --git a/rockwork/NotificationsPage.qml b/rockwork/NotificationsPage.qml
new file mode 100644
index 0000000..9802b05
--- /dev/null
+++ b/rockwork/NotificationsPage.qml
@@ -0,0 +1,88 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import RockWork 1.0
+
+Page {
+ id: root
+ title: i18n.tr("Notifications")
+
+ property var pebble: null
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.topMargin: units.gu(1)
+
+ Item {
+ Layout.fillWidth: true
+ implicitHeight: infoLabel.height
+
+ Label {
+ id: infoLabel
+ anchors {
+ left: parent.left
+ right: parent.right
+ margins: units.gu(2)
+ }
+
+ wrapMode: Text.WordWrap
+ text: i18n.tr("Entries here will be added as notifications appear on the phone. Selected notifications will be shown on your Pebble smartwatch.")
+ }
+ }
+
+
+ ListView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ model: root.pebble.notifications
+
+ delegate: ListItem {
+ ListItemLayout {
+ title.text: model.name
+
+ UbuntuShape {
+ SlotsLayout.position: SlotsLayout.Leading;
+ height: units.gu(5)
+ width: height
+ backgroundColor: {
+ // Add some hacks for known icons
+ switch (model.icon) {
+ case "calendar":
+ return UbuntuColors.orange;
+ case "settings":
+ return "grey";
+ case "dialog-question-symbolic":
+ return UbuntuColors.red;
+ case "alarm-clock":
+ return UbuntuColors.purple;
+ case "gpm-battery-050":
+ return UbuntuColors.green;
+ }
+ return "black"
+ }
+ source: Image {
+ height: parent.height
+ width: parent.width
+ source: model.icon.indexOf("/") === 0 ? "file://" + model.icon : ""
+ }
+ Icon {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ name: model.icon.indexOf("/") !== 0 ? model.icon : ""
+ color: "white"
+ }
+ }
+
+ Switch {
+ checked: model.enabled
+ SlotsLayout.position: SlotsLayout.Trailing;
+ onClicked: {
+ root.pebble.setNotificationFilter(model.name, checked)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/rockwork/PebbleModels.qml b/rockwork/PebbleModels.qml
new file mode 100644
index 0000000..103064a
--- /dev/null
+++ b/rockwork/PebbleModels.qml
@@ -0,0 +1,28 @@
+import QtQuick 2.4
+
+ListModel {
+ id: modelModel
+ ListElement { image: 'artwork/tintin-black.png'; shape: "rectangle" } // Fallback for Unknown
+ ListElement { image: 'artwork/tintin-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-white.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-red.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-orange.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-grey.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bianca-silver.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bianca-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-blue.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-green.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/tintin-pink.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/snowy-white.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/snowy-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/snowy-red.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bobby-silver.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bobby-black.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/bobby-gold.png'; shape: "rectangle" }
+ ListElement { image: 'artwork/spalding-14mm-silver.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-14mm-black.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-20mm-silver.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-20mm-black.png'; shape: "round" }
+ ListElement { image: 'artwork/spalding-14mm-rose-gold.png'; shape: "round" }
+}
+
diff --git a/rockwork/PebblesPage.qml b/rockwork/PebblesPage.qml
new file mode 100644
index 0000000..a973b0a
--- /dev/null
+++ b/rockwork/PebblesPage.qml
@@ -0,0 +1,69 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+
+Page {
+ title: i18n.tr("Manage Pebble Watches")
+
+ head {
+ actions: [
+ Action {
+ iconName: "settings"
+ onTriggered: {
+ onClicked: Qt.openUrlExternally("settings://system/bluetooth")
+ }
+ }
+ ]
+ }
+
+ ListView {
+ anchors.fill: parent
+ model: pebbles
+ delegate: ListItem {
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+
+ ColumnLayout {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ Label {
+ text: model.name
+ }
+
+ Label {
+ text: model.connected ? i18n.tr("Connected") : i18n.tr("Disconnected")
+ fontSize: "small"
+ }
+ }
+ }
+
+ onClicked: {
+ var p = pebbles.get(index);
+ print("opening pebble:", p.name, p.hardwarePlatform)
+ pageStack.push(Qt.resolvedUrl("MainMenuPage.qml"), {pebble: pebbles.get(index)})
+ }
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ width: parent.width - units.gu(4)
+ spacing: units.gu(4)
+ visible: pebbles.count === 0
+
+ Label {
+ text: i18n.tr("No Pebble smartwatches configured yet. Please connect your Pebble smartwatch using System Settings.")
+ fontSize: "large"
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ Button {
+ text: i18n.tr("Open System Settings")
+ anchors.horizontalCenter: parent.horizontalCenter
+ onClicked: Qt.openUrlExternally("settings://system/bluetooth")
+ }
+ }
+}
diff --git a/rockwork/ScreenshotsPage.qml b/rockwork/ScreenshotsPage.qml
new file mode 100644
index 0000000..fdbeb9a
--- /dev/null
+++ b/rockwork/ScreenshotsPage.qml
@@ -0,0 +1,107 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.Popups 1.3
+import Ubuntu.Content 1.3
+import RockWork 1.0
+
+Page {
+ id: root
+
+ title: i18n.tr("Screenshots")
+
+ property var pebble: null
+
+ head {
+ actions: [
+ Action {
+ iconName: "camera-app-symbolic"
+ onTriggered: root.pebble.requestScreenshot()
+ }
+ ]
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ GridView {
+ id: grid
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ clip: true
+
+ property int columns: 2
+
+ cellWidth: width / columns
+ cellHeight: cellWidth
+
+ model: root.pebble.screenshots
+
+ displaced: Transition {
+ UbuntuNumberAnimation { properties: "x,y" }
+ }
+
+ delegate: Item {
+ width: grid.cellWidth
+ height: grid.cellHeight
+ Image {
+ anchors.fill: parent
+ anchors.margins: units.gu(.5)
+ fillMode: Image.PreserveAspectFit
+ source: "file://" + model.filename
+ }
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ PopupUtils.open(dialogComponent, root, {filename: model.filename})
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: dialogComponent
+ Dialog {
+ id: dialog
+ title: i18n.tr("Screenshot options")
+
+ property string filename
+
+ Button {
+ text: i18n.tr("Share")
+ color: UbuntuColors.blue
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("Pebble screenshot"), handler: ContentHandler.Share, contentType: ContentType.Pictures, filename: filename })
+ PopupUtils.close(dialog)
+ }
+ }
+ Button {
+ text: i18n.tr("Save")
+ color: UbuntuColors.green
+ onClicked: {
+ pageStack.push(Qt.resolvedUrl("ContentPeerPickerPage.qml"), {itemName: i18n.tr("Pebble screenshot"),handler: ContentHandler.Destination, contentType: ContentType.Pictures, filename: filename })
+ PopupUtils.close(dialog)
+ }
+ }
+
+ Button {
+ text: i18n.tr("Delete")
+ color: UbuntuColors.red
+ onClicked: {
+ root.pebble.removeScreenshot(filename)
+ PopupUtils.close(dialog)
+ }
+ }
+ Button {
+ text: i18n.tr("Cancel")
+ onClicked: {
+ PopupUtils.close(dialog)
+ }
+ }
+ }
+ }
+}
+
diff --git a/rockwork/SettingsPage.qml b/rockwork/SettingsPage.qml
new file mode 100644
index 0000000..153aaf4
--- /dev/null
+++ b/rockwork/SettingsPage.qml
@@ -0,0 +1,80 @@
+import QtQuick 2.4
+import QtQuick.Layouts 1.1
+import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 1.3
+
+Page {
+ id: root
+ title: i18n.tr("Settings")
+
+ property var pebble: null
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: units.gu(1)
+ spacing: units.gu(1)
+
+ Label {
+ Layout.fillWidth: true
+ text: i18n.tr("Distance Units")
+ font.bold: true
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ CheckBox {
+ id: metricUnitsCheckbox
+ checked: !root.pebble.imperialUnits
+ onClicked: {
+ checked = true
+ root.pebble.imperialUnits = false;
+ imperialUnitsCheckBox.checked = false;
+ }
+ }
+ Label {
+ text: i18n.tr("Metric")
+ Layout.fillWidth: true
+ }
+ CheckBox {
+ id: imperialUnitsCheckBox
+ checked: root.pebble.imperialUnits
+ onClicked: {
+ checked = true
+ root.pebble.imperialUnits = true;
+ metricUnitsCheckbox.checked = false;
+ }
+ }
+ Label {
+ text: i18n.tr("Imperial")
+ Layout.fillWidth: true
+ }
+ }
+ ThinDivider {}
+
+ Label {
+ text: i18n.tr("Calendar")
+ Layout.fillWidth: true
+ font.bold: true
+ }
+ RowLayout {
+ Layout.fillWidth: true
+ Label {
+ text: i18n.tr("Sync calendar to timeline")
+ Layout.fillWidth: true
+ }
+ Switch {
+ checked: root.pebble.calendarSyncEnabled
+ onClicked: {
+ root.pebble.calendarSyncEnabled = checked;
+ }
+ }
+ }
+ ThinDivider {}
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+}
+
diff --git a/rockwork/SystemAppIcon.qml b/rockwork/SystemAppIcon.qml
new file mode 100644
index 0000000..88e37bc
--- /dev/null
+++ b/rockwork/SystemAppIcon.qml
@@ -0,0 +1,67 @@
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+Item {
+ id: root
+
+ property bool isSystemApp: false
+ property string uuid: ""
+ property string iconSource: ""
+
+ UbuntuShape {
+ anchors.fill: parent
+ visible: root.isSystemApp
+ backgroundColor: {
+ switch (root.uuid) {
+ case "{07e0d9cb-8957-4bf7-9d42-35bf47caadfe}":
+ return "gray";
+ case "{18e443ce-38fd-47c8-84d5-6d0c775fbe55}":
+ return "blue";
+ case "{36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c}":
+ return UbuntuColors.red;
+ case "{1f03293d-47af-4f28-b960-f2b02a6dd757}":
+ return "gold"
+ case "{b2cae818-10f8-46df-ad2b-98ad2254a3c1}":
+ return "darkviolet"
+ case "{67a32d95-ef69-46d4-a0b9-854cc62f97f9}":
+ return "green";
+ case "{8f3c8686-31a1-4f5f-91f5-01600c9bdc59}":
+ return "black"
+ }
+
+ return "";
+ }
+ }
+ Icon {
+ anchors.fill: parent
+ implicitHeight: height
+ anchors.margins: units.gu(1)
+ visible: root.isSystemApp
+ color: "white"
+ name: {
+ switch (root.uuid) {
+ case "{07e0d9cb-8957-4bf7-9d42-35bf47caadfe}":
+ return "settings";
+ case "{18e443ce-38fd-47c8-84d5-6d0c775fbe55}":
+ return "clock-app-symbolic";
+ case "{36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c}":
+ return "like";
+ case "{1f03293d-47af-4f28-b960-f2b02a6dd757}":
+ return "stock_music";
+ case "{b2cae818-10f8-46df-ad2b-98ad2254a3c1}":
+ return "stock_notification";
+ case "{67a32d95-ef69-46d4-a0b9-854cc62f97f9}":
+ return "stock_alarm-clock";
+ case "{8f3c8686-31a1-4f5f-91f5-01600c9bdc59}":
+ return "clock-app-symbolic";
+ }
+ return "";
+ }
+ }
+
+ Image {
+ source: root.isSystemApp ? "" : "file://" + root.iconSource;
+ anchors.fill: parent
+ visible: !root.isSystemApp
+ }
+}
diff --git a/rockwork/applicationsfiltermodel.cpp b/rockwork/applicationsfiltermodel.cpp
new file mode 100644
index 0000000..d3eb10d
--- /dev/null
+++ b/rockwork/applicationsfiltermodel.cpp
@@ -0,0 +1,102 @@
+#include "applicationsfiltermodel.h"
+#include "applicationsmodel.h"
+
+ApplicationsFilterModel::ApplicationsFilterModel(QObject *parent):
+ QSortFilterProxyModel(parent)
+{
+ sort(0);
+}
+
+ApplicationsModel *ApplicationsFilterModel::appsModel() const
+{
+ return m_appsModel;
+}
+
+void ApplicationsFilterModel::setAppsModel(ApplicationsModel *model)
+{
+ if (m_appsModel != model) {
+ m_appsModel = model;
+ setSourceModel(m_appsModel);
+ emit appsModelChanged();
+ }
+}
+
+bool ApplicationsFilterModel::showWatchApps() const
+{
+ return m_showWatchApps;
+}
+
+void ApplicationsFilterModel::setShowWatchApps(bool showWatchApps)
+{
+ if (m_showWatchApps != showWatchApps) {
+ m_showWatchApps = showWatchApps;
+ emit showWatchAppsChanged();
+ invalidateFilter();
+ }
+}
+
+bool ApplicationsFilterModel::showWatchFaces() const
+{
+ return m_showWatchFaces;
+}
+
+void ApplicationsFilterModel::setShowWatchFaces(bool showWatchFaces)
+{
+ if (m_showWatchFaces != showWatchFaces) {
+ m_showWatchFaces = showWatchFaces;
+ emit showWatchFacesChanged();
+ invalidateFilter();
+ }
+}
+
+bool ApplicationsFilterModel::sortByGroupId() const
+{
+ return m_sortByGroupId;
+}
+
+void ApplicationsFilterModel::setSortByGroupId(bool sortByGroupId)
+{
+ if (m_sortByGroupId != sortByGroupId) {
+ m_sortByGroupId = sortByGroupId;
+ emit sortByGroupIdChanged();
+ sort(0);
+ }
+}
+
+bool ApplicationsFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ Q_UNUSED(source_parent)
+ AppItem *item = m_appsModel->get(source_row);
+ if (m_showWatchApps && !item->isWatchFace()) {
+ return true;
+ }
+ if (m_showWatchFaces && item->isWatchFace()) {
+ return true;
+ }
+ return false;
+}
+
+bool ApplicationsFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
+{
+ AppItem *leftItem = m_appsModel->get(source_left.row());
+ AppItem *rightItem = m_appsModel->get(source_right.row());
+
+ if (m_sortByGroupId && leftItem->groupId() != rightItem->groupId()) {
+ return leftItem->groupId() < rightItem->groupId();
+ }
+
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+}
+
+AppItem* ApplicationsFilterModel::get(int index) const
+{
+ return m_appsModel->get(mapToSource(this->index(index, 0)).row());
+}
+
+void ApplicationsFilterModel::move(int from, int to)
+{
+ QModelIndex sourceFrom = mapToSource(index(from, 0));
+ QModelIndex sourceTo = mapToSource(index(to, 0));
+ m_appsModel->move(sourceFrom.row(), sourceTo.row());
+}
+
diff --git a/rockwork/applicationsfiltermodel.h b/rockwork/applicationsfiltermodel.h
new file mode 100644
index 0000000..96c3b5c
--- /dev/null
+++ b/rockwork/applicationsfiltermodel.h
@@ -0,0 +1,54 @@
+#ifndef APPLICATIONSFILTERMODEL_H
+#define APPLICATIONSFILTERMODEL_H
+
+#include <QSortFilterProxyModel>
+
+class ApplicationsModel;
+class AppItem;
+
+class ApplicationsFilterModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+ Q_PROPERTY(ApplicationsModel* model READ appsModel WRITE setAppsModel NOTIFY appsModelChanged)
+ Q_PROPERTY(bool showWatchApps READ showWatchApps WRITE setShowWatchApps NOTIFY showWatchAppsChanged)
+ Q_PROPERTY(bool showWatchFaces READ showWatchFaces WRITE setShowWatchFaces NOTIFY showWatchFacesChanged)
+ Q_PROPERTY(bool sortByGroupId READ sortByGroupId WRITE setSortByGroupId NOTIFY sortByGroupIdChanged)
+
+public:
+ ApplicationsFilterModel(QObject *parent = nullptr);
+
+ ApplicationsModel *appsModel() const;
+ void setAppsModel(ApplicationsModel *model);
+
+ bool showWatchApps() const;
+ void setShowWatchApps(bool showWatchApps);
+
+ bool showWatchFaces() const;
+ void setShowWatchFaces(bool showWatchFaces);
+
+ bool sortByGroupId() const;
+ void setSortByGroupId(bool sortByGroupId);
+
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
+ bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
+
+ Q_INVOKABLE AppItem *get(int index) const;
+
+ Q_INVOKABLE void move(int from, int to);
+signals:
+ void appsModelChanged();
+ void showWatchAppsChanged();
+ void showWatchFacesChanged();
+ void sortByGroupIdChanged();
+
+public slots:
+
+private:
+ ApplicationsModel *m_appsModel;
+
+ bool m_showWatchApps = true;
+ bool m_showWatchFaces = true;
+ bool m_sortByGroupId = true;
+};
+
+#endif // APPLICATIONSFILTERMODEL_H
diff --git a/rockwork/applicationsmodel.cpp b/rockwork/applicationsmodel.cpp
new file mode 100644
index 0000000..27e2c3e
--- /dev/null
+++ b/rockwork/applicationsmodel.cpp
@@ -0,0 +1,365 @@
+#include "applicationsmodel.h"
+
+#include <QDebug>
+
+
+ApplicationsModel::ApplicationsModel(QObject *parent):
+ QAbstractListModel(parent)
+{
+}
+
+int ApplicationsModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_apps.count();
+}
+
+QVariant ApplicationsModel::data(const QModelIndex &index, int role) const
+{
+ switch (role) {
+ case RoleStoreId:
+ return m_apps.at(index.row())->storeId();
+ case RoleUuid:
+ return m_apps.at(index.row())->uuid();
+ case RoleName:
+ return m_apps.at(index.row())->name();
+ case RoleIcon:
+ return m_apps.at(index.row())->icon();
+ case RoleVendor:
+ return m_apps.at(index.row())->vendor();
+ case RoleVersion:
+ return m_apps.at(index.row())->version();
+ case RoleIsWatchFace:
+ return m_apps.at(index.row())->isWatchFace();
+ case RoleIsSystemApp:
+ return m_apps.at(index.row())->isSystemApp();
+ case RoleHasSettings:
+ return m_apps.at(index.row())->hasSettings();
+ case RoleDescription:
+ return m_apps.at(index.row())->description();
+ case RoleHearts:
+ return m_apps.at(index.row())->hearts();
+ case RoleCategory:
+ return m_apps.at(index.row())->category();
+ case RoleGroupId:
+ return m_apps.at(index.row())->groupId();
+ }
+
+ return QVariant();
+}
+
+QHash<int, QByteArray> ApplicationsModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles.insert(RoleStoreId, "storeId");
+ roles.insert(RoleUuid, "uuid");
+ roles.insert(RoleName, "name");
+ roles.insert(RoleIcon, "icon");
+ roles.insert(RoleVendor, "vendor");
+ roles.insert(RoleVersion, "version");
+ roles.insert(RoleIsWatchFace, "isWatchFace");
+ roles.insert(RoleIsSystemApp, "isSystemApp");
+ roles.insert(RoleHasSettings, "hasSettings");
+ roles.insert(RoleDescription, "description");
+ roles.insert(RoleHearts, "hearts");
+ roles.insert(RoleCategory, "category");
+ roles.insert(RoleGroupId, "groupId");
+ return roles;
+}
+
+void ApplicationsModel::clear()
+{
+ beginResetModel();
+ qDeleteAll(m_apps);
+ m_apps.clear();
+ endResetModel();
+ m_groupNames.clear();
+ m_groupLinks.clear();
+ m_links.clear();
+ m_linkNames.clear();
+ emit linksChanged();
+ emit changed();
+}
+
+void ApplicationsModel::insert(AppItem *item)
+{
+ item->setParent(this);
+ beginInsertRows(QModelIndex(), rowCount(), rowCount());
+ m_apps.append(item);
+ endInsertRows();
+ emit changed();
+}
+
+void ApplicationsModel::insertGroup(const QString &id, const QString &name, const QString &link)
+{
+ m_groupNames[id] = name;
+ m_groupLinks[id] = link;
+}
+
+AppItem *ApplicationsModel::get(int index) const
+{
+ if (index >= 0 && index < m_apps.count()) {
+ return m_apps.at(index);
+ }
+ return nullptr;
+}
+
+AppItem *ApplicationsModel::findByStoreId(const QString &storeId) const
+{
+ foreach (AppItem *item, m_apps) {
+ if (item->storeId() == storeId) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+AppItem *ApplicationsModel::findByUuid(const QString &uuid) const
+{
+ foreach (AppItem *item, m_apps) {
+ if (item->uuid() == uuid) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+bool ApplicationsModel::contains(const QString &storeId) const
+{
+ foreach (AppItem* item, m_apps) {
+ if (item->storeId() == storeId) {
+ return true;
+ }
+ }
+ return false;
+}
+
+int ApplicationsModel::indexOf(AppItem *item) const
+{
+ return m_apps.indexOf(item);
+}
+
+QString ApplicationsModel::groupName(const QString &groupId) const
+{
+ return m_groupNames.value(groupId);
+}
+
+QString ApplicationsModel::groupLink(const QString &groupId) const
+{
+ return m_groupLinks.value(groupId);
+}
+
+QString ApplicationsModel::linkName(const QString &link) const
+{
+ return m_linkNames.value(link);
+}
+
+QStringList ApplicationsModel::links() const
+{
+ return m_links;
+}
+
+void ApplicationsModel::addLink(const QString &link, const QString &name)
+{
+ m_links.append(link);
+ m_linkNames[link] = name;
+ emit linksChanged();
+}
+
+void ApplicationsModel::move(int from, int to)
+{
+ if (from < 0 || to < 0) {
+ return;
+ }
+ if (from >= m_apps.count() || to >= m_apps.count()) {
+ return;
+ }
+ if (from == to) {
+ return;
+ }
+ int newModelIndex = to > from ? to + 1 : to;
+ beginMoveRows(QModelIndex(), from, from, QModelIndex(), newModelIndex);
+
+ m_apps.move(from, to);
+ QStringList appList;
+ foreach (const AppItem *item, m_apps) {
+ appList << item->name();
+ }
+ endMoveRows();
+}
+
+void ApplicationsModel::commitMove()
+{
+ emit appsSorted();
+}
+
+AppItem::AppItem(QObject *parent):
+ QObject(parent)
+{
+
+}
+
+QString AppItem::storeId() const
+{
+ return m_storeId;
+}
+
+QString AppItem::uuid() const
+{
+ return m_uuid;
+}
+
+QString AppItem::name() const
+{
+ return m_name;
+}
+
+QString AppItem::icon() const
+{
+ return m_icon;
+}
+
+QString AppItem::vendor() const
+{
+ return m_vendor;
+}
+
+QString AppItem::version() const
+{
+ return m_version;
+}
+
+QString AppItem::description() const
+{
+ return m_description;
+}
+
+int AppItem::hearts() const
+{
+ return m_hearts;
+}
+
+QStringList AppItem::screenshotImages() const
+{
+ return m_screenshotImages;
+}
+
+bool AppItem::isWatchFace() const
+{
+ return m_isWatchFace;
+}
+
+bool AppItem::isSystemApp() const
+{
+ return m_isSystemApp;
+}
+
+bool AppItem::hasSettings() const
+{
+ return m_hasSettings;
+}
+
+bool AppItem::companion() const
+{
+ return m_companion;
+}
+
+QString AppItem::category() const
+{
+ return m_category;
+}
+
+QString AppItem::groupId() const
+{
+ return m_groupId;
+}
+
+void AppItem::setStoreId(const QString &storeId)
+{
+ m_storeId = storeId;
+}
+
+void AppItem::setUuid(const QString &uuid)
+{
+ m_uuid = uuid;
+}
+
+void AppItem::setName(const QString &name)
+{
+ m_name = name;
+}
+
+void AppItem::setIcon(const QString &icon)
+{
+ m_icon = icon;
+}
+
+void AppItem::setVendor(const QString &vendor)
+{
+ m_vendor = vendor;
+ emit vendorChanged();
+}
+
+void AppItem::setVersion(const QString &version)
+{
+ m_version = version;
+ emit versionChanged();
+}
+
+void AppItem::setDescription(const QString &description)
+{
+ m_description = description;
+}
+
+void AppItem::setHearts(int hearts)
+{
+ m_hearts = hearts;
+}
+
+void AppItem::setIsWatchFace(bool isWatchFace)
+{
+ m_isWatchFace = isWatchFace;
+ emit isWatchFaceChanged();
+}
+
+void AppItem::setIsSystemApp(bool isSystemApp)
+{
+ m_isSystemApp = isSystemApp;
+}
+
+void AppItem::setHasSettings(bool hasSettings)
+{
+ m_hasSettings = hasSettings;
+}
+
+void AppItem::setCompanion(bool companion)
+{
+ m_companion = companion;
+}
+
+void AppItem::setCategory(const QString &category)
+{
+ m_category = category;
+}
+
+void AppItem::setScreenshotImages(const QStringList &screenshotImages)
+{
+ m_screenshotImages = screenshotImages;
+}
+
+void AppItem::setHeaderImage(const QString &headerImage)
+{
+ m_headerImage = headerImage;
+ emit headerImageChanged();
+}
+
+void AppItem::setGroupId(const QString &groupId)
+{
+ m_groupId = groupId;
+}
+
+QString AppItem::headerImage() const
+{
+ return m_headerImage;
+}
+
diff --git a/rockwork/applicationsmodel.h b/rockwork/applicationsmodel.h
new file mode 100644
index 0000000..91539bc
--- /dev/null
+++ b/rockwork/applicationsmodel.h
@@ -0,0 +1,160 @@
+#ifndef APPLICATIONSMODEL_H
+#define APPLICATIONSMODEL_H
+
+#include <QAbstractListModel>
+#include <QDBusObjectPath>
+
+class QDBusInterface;
+
+class AppItem: public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString storeId MEMBER m_storeId CONSTANT)
+ Q_PROPERTY(QString uuid MEMBER m_uuid CONSTANT)
+ Q_PROPERTY(QString name MEMBER m_name CONSTANT)
+ Q_PROPERTY(QString icon MEMBER m_icon CONSTANT)
+ Q_PROPERTY(QString vendor MEMBER m_vendor NOTIFY vendorChanged)
+ Q_PROPERTY(QString version MEMBER m_version NOTIFY versionChanged)
+ Q_PROPERTY(QString description MEMBER m_description CONSTANT)
+ Q_PROPERTY(int hearts MEMBER m_hearts CONSTANT)
+ Q_PROPERTY(QStringList screenshotImages MEMBER m_screenshotImages CONSTANT)
+ Q_PROPERTY(QString headerImage READ headerImage NOTIFY headerImageChanged)
+ Q_PROPERTY(QString category MEMBER m_category CONSTANT)
+ Q_PROPERTY(bool isWatchFace MEMBER m_isWatchFace NOTIFY isWatchFaceChanged)
+ Q_PROPERTY(bool isSystemApp MEMBER m_isSystemApp CONSTANT)
+ Q_PROPERTY(bool hasSettings MEMBER m_hasSettings CONSTANT)
+ Q_PROPERTY(bool companion MEMBER m_companion CONSTANT)
+
+ Q_PROPERTY(QString groupId MEMBER m_groupId CONSTANT)
+
+
+public:
+ AppItem(QObject *parent = 0);
+
+ QString storeId() const;
+ QString uuid() const;
+ QString name() const;
+ QString icon() const;
+ QString vendor() const;
+ QString version() const;
+ QString description() const;
+ int hearts() const;
+ QStringList screenshotImages() const;
+ QString headerImage() const;
+ bool isWatchFace() const;
+ bool isSystemApp() const;
+ bool hasSettings() const;
+ bool companion() const;
+ QString category() const;
+
+ QString groupId() const;
+
+ void setStoreId(const QString &storeId);
+ void setUuid(const QString &uuid);
+ void setName(const QString &name);
+ void setIcon(const QString &icon);
+ void setVendor(const QString &vendor);
+ void setVersion(const QString &version);
+ void setDescription(const QString &description);
+ void setHearts(int hearts);
+ void setCategory(const QString &category);
+ void setScreenshotImages(const QStringList &screenshotImages);
+ void setHeaderImage(const QString &headerImage);
+ void setIsWatchFace(bool isWatchFace);
+ void setIsSystemApp(bool isSystemApp);
+ void setHasSettings(bool hasSettings);
+ void setCompanion(bool companion);
+
+ // For grouping in lists, e.g. by collection
+ void setGroupId(const QString &groupId);
+
+
+signals:
+ void versionChanged();
+ void vendorChanged();
+ void headerImageChanged();
+ void isWatchFaceChanged();
+
+private:
+ QString m_storeId;
+ QString m_uuid;
+ QString m_name;
+ QString m_icon;
+ QString m_vendor;
+ QString m_version;
+ QString m_description;
+ int m_hearts = 0;
+ QString m_category;
+ QStringList m_screenshotImages;
+ bool m_isWatchFace = false;
+ bool m_isSystemApp = false;
+ bool m_hasSettings = false;
+ bool m_companion = false;
+
+ QString m_groupId;
+
+ QString m_headerImage;
+};
+
+class ApplicationsModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QStringList links READ links NOTIFY linksChanged)
+
+public:
+ enum Roles {
+ RoleStoreId,
+ RoleUuid,
+ RoleName,
+ RoleIcon,
+ RoleVendor,
+ RoleVersion,
+ RoleIsWatchFace,
+ RoleIsSystemApp,
+ RoleHasSettings,
+ RoleDescription,
+ RoleHearts,
+ RoleCategory,
+ RoleGroupId
+ };
+
+ ApplicationsModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ void clear();
+ void insert(AppItem *item);
+ void insertGroup(const QString &id, const QString &name, const QString &link);
+
+ Q_INVOKABLE AppItem* get(int index) const;
+ AppItem* findByStoreId(const QString &storeId) const;
+ AppItem* findByUuid(const QString &uuid) const;
+ Q_INVOKABLE bool contains(const QString &storeId) const;
+ int indexOf(AppItem *item) const;
+
+ Q_INVOKABLE QString groupName(const QString &groupId) const;
+ Q_INVOKABLE QString groupLink(const QString &groupId) const;
+
+ QStringList links() const;
+ Q_INVOKABLE QString linkName(const QString &link) const;
+ void addLink(const QString &link, const QString &name);
+
+ Q_INVOKABLE void move(int from, int to);
+ Q_INVOKABLE void commitMove();
+
+signals:
+ void linksChanged();
+ void appsSorted();
+ void changed();
+
+private:
+ QList<AppItem*> m_apps;
+ QHash<QString, QString> m_groupNames;
+ QHash<QString, QString> m_groupLinks;
+ QStringList m_links;
+ QHash<QString, QString> m_linkNames;
+};
+
+#endif // APPLICATIONSMODEL_H
diff --git a/rockwork/appstoreclient.cpp b/rockwork/appstoreclient.cpp
new file mode 100644
index 0000000..ac87510
--- /dev/null
+++ b/rockwork/appstoreclient.cpp
@@ -0,0 +1,323 @@
+#include "appstoreclient.h"
+#include "applicationsmodel.h"
+
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QUrlQuery>
+#include <QJsonDocument>
+#include <QJsonParseError>
+
+#include <libintl.h>
+
+/* Known params for pebble api
+ query.addQueryItem("offset", QString::number(offset));
+ query.addQueryItem("limit", QString::number(limit));
+ query.addQueryItem("image_ratio", "1"); // Not sure yet what this does
+ query.addQueryItem("filter_hardware", "true");
+ query.addQueryItem("firmware_version", "3");
+ query.addQueryItem("hardware", hardwarePlatform);
+ query.addQueryItem("platform", "all");
+*/
+
+AppStoreClient::AppStoreClient(QObject *parent):
+ QObject(parent),
+ m_nam(new QNetworkAccessManager(this)),
+ m_model(new ApplicationsModel(this))
+{
+}
+
+ApplicationsModel *AppStoreClient::model() const
+{
+ return m_model;
+}
+
+int AppStoreClient::limit() const
+{
+ return m_limit;
+}
+
+void AppStoreClient::setLimit(int limit)
+{
+ m_limit = limit;
+ emit limitChanged();
+}
+
+QString AppStoreClient::hardwarePlatform() const
+{
+ return m_hardwarePlatform;
+}
+
+void AppStoreClient::setHardwarePlatform(const QString &hardwarePlatform)
+{
+ m_hardwarePlatform = hardwarePlatform;
+ emit hardwarePlatformChanged();
+}
+
+bool AppStoreClient::busy() const
+{
+ return m_busy;
+}
+
+void AppStoreClient::fetchHome(Type type)
+{
+ m_model->clear();
+ setBusy(true);
+
+ QUrlQuery query;
+ query.addQueryItem("firmware_version", "3");
+ if (!m_hardwarePlatform.isEmpty()) {
+ query.addQueryItem("hardware", m_hardwarePlatform);
+ query.addQueryItem("filter_hardware", "true");
+ }
+
+ QString url;
+ if (type == TypeWatchapp) {
+ url = "https://api2.getpebble.com/v2/home/apps";
+ } else {
+ url = "https://api2.getpebble.com/v2/home/watchfaces";
+ }
+ QUrl storeUrl(url);
+ storeUrl.setQuery(query);
+ QNetworkRequest request(storeUrl);
+
+ qDebug() << "fetching home" << storeUrl.toString();
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+
+ QHash<QString, QStringList> collections;
+ foreach (const QVariant &entry, resultMap.value("collections").toList()) {
+ QStringList appIds;
+ foreach (const QVariant &appId, entry.toMap().value("application_ids").toList()) {
+ appIds << appId.toString();
+ }
+ QString slug = entry.toMap().value("slug").toString();
+ collections[slug] = appIds;
+ m_model->insertGroup(slug, entry.toMap().value("name").toString(), entry.toMap().value("links").toMap().value("apps").toString());
+ }
+
+ QHash<QString, QString> categoryNames;
+ foreach (const QVariant &entry, resultMap.value("categories").toList()) {
+ categoryNames[entry.toMap().value("id").toString()] = entry.toMap().value("name").toString();
+ }
+
+ foreach (const QVariant &entry, jsonDoc.toVariant().toMap().value("applications").toList()) {
+ AppItem* item = parseAppItem(entry.toMap());
+ foreach (const QString &collection, collections.keys()) {
+ if (collections.value(collection).contains(item->storeId())) {
+ item->setGroupId(collection);
+ break;
+ }
+ }
+ item->setCategory(categoryNames.value(entry.toMap().value("category_id").toString()));
+
+ qDebug() << "have entry" << item->name() << item->groupId() << item->companion();
+
+ if (item->groupId().isEmpty() || item->companion()) {
+ // Skip items that we couldn't match to a collection
+ // Also skip apps that need a companion
+ delete item;
+ continue;
+ }
+ m_model->insert(item);
+ }
+ setBusy(false);
+ });
+
+
+}
+
+void AppStoreClient::fetchLink(const QString &link)
+{
+ m_model->clear();
+ setBusy(true);
+
+ QUrl storeUrl(link);
+ QUrlQuery query(storeUrl);
+ query.removeQueryItem("limit");
+ // We fetch one more than we actually want so we can see if we need to display
+ // a next button
+ query.addQueryItem("limit", QString::number(m_limit + 1));
+ int currentOffset = query.queryItemValue("offset").toInt();
+ query.removeQueryItem("offset");
+ query.addQueryItem("offset", QString::number(qMax(0, currentOffset - 1)));
+ if (!query.hasQueryItem("hardware")) {
+ query.addQueryItem("hardware", m_hardwarePlatform);
+ query.addQueryItem("filter_hardware", "true");
+ }
+ storeUrl.setQuery(query);
+ QNetworkRequest request(storeUrl);
+ qDebug() << "fetching link" << request.url();
+
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ qDebug() << "fetch reply";
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+
+ bool haveMore = false;
+ foreach (const QVariant &entry, resultMap.value("data").toList()) {
+ if (model()->rowCount() >= m_limit) {
+ haveMore = true;
+ break;
+ }
+ AppItem *item = parseAppItem(entry.toMap());
+ if (item->companion()) {
+ // For now just skip items with companions
+ delete item;
+ } else {
+ m_model->insert(item);
+ }
+ }
+
+ if (resultMap.contains("links") && resultMap.value("links").toMap().contains("nextPage") &&
+ !resultMap.value("links").toMap().value("nextPage").isNull()) {
+ int currentOffset = resultMap.value("offset").toInt();
+ QString nextLink = resultMap.value("links").toMap().value("nextPage").toString();
+
+ if (currentOffset > 0) {
+ QUrl previousLink(nextLink);
+ QUrlQuery query(previousLink);
+ query.removeQueryItem("limit");
+ query.addQueryItem("limit", QString::number(m_limit + 1));
+ query.removeQueryItem("offset");
+ query.addQueryItem("offset", QString::number(qMax(0, currentOffset - m_limit + 1)));
+ previousLink.setQuery(query);
+ m_model->addLink(previousLink.toString(), gettext("Previous"));
+ }
+ if (haveMore) {
+ m_model->addLink(nextLink, gettext("Next"));
+ }
+ }
+ setBusy(false);
+ });
+
+}
+
+void AppStoreClient::fetchAppDetails(const QString &appId)
+{
+ QUrl url("https://api2.getpebble.com/v2/apps/id/" + appId);
+ QUrlQuery query;
+ if (!m_hardwarePlatform.isEmpty()) {
+ query.addQueryItem("hardware", m_hardwarePlatform);
+ }
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ QNetworkReply * reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply, appId]() {
+ reply->deleteLater();
+ AppItem *item = m_model->findByStoreId(appId);
+ if (!item) {
+ qWarning() << "Can't find item with id" << appId;
+ return;
+ }
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
+ QVariantMap replyMap = jsonDoc.toVariant().toMap().value("data").toList().first().toMap();
+ if (replyMap.contains("header_images") && replyMap.value("header_images").toList().count() > 0) {
+ item->setHeaderImage(replyMap.value("header_images").toList().first().toMap().value("orig").toString());
+ }
+ item->setVendor(replyMap.value("author").toString());
+ item->setVersion(replyMap.value("latest_release").toMap().value("version").toString());
+ item->setIsWatchFace(replyMap.value("type").toString() == "watchface");
+ });
+}
+
+void AppStoreClient::search(const QString &searchString, Type type)
+{
+ m_model->clear();
+ setBusy(true);
+
+ QUrl url("https://bujatnzd81-dsn.algolia.io/1/indexes/pebble-appstore-production");
+ QUrlQuery query;
+ query.addQueryItem("x-algolia-api-key", "8dbb11cdde0f4f9d7bf787e83ac955ed");
+ query.addQueryItem("x-algolia-application-id", "BUJATNZD81");
+ query.addQueryItem("query", searchString);
+ QStringList filters;
+ if (type == TypeWatchapp) {
+ filters.append("watchapp");
+ } else if (type == TypeWatchface) {
+ filters.append("watchface");
+ }
+ filters.append(m_hardwarePlatform);
+ query.addQueryItem("tagFilters", filters.join(","));
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ qDebug() << "Search query:" << url;
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ m_model->clear();
+ setBusy(false);
+
+ reply->deleteLater();
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
+
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+ foreach (const QVariant &entry, resultMap.value("hits").toList()) {
+ AppItem *item = parseAppItem(entry.toMap());
+ m_model->insert(item);
+// qDebug() << "have item" << item->name() << item->icon();
+ }
+ qDebug() << "Found" << m_model->rowCount() << "items";
+ });
+}
+
+AppItem* AppStoreClient::parseAppItem(const QVariantMap &map)
+{
+ AppItem *item = new AppItem();
+ item->setStoreId(map.value("id").toString());
+ item->setName(map.value("title").toString());
+ if (!map.value("list_image").toString().isEmpty()) {
+ item->setIcon(map.value("list_image").toString());
+ } else {
+ item->setIcon(map.value("list_image").toMap().value("144x144").toString());
+ }
+ item->setDescription(map.value("description").toString());
+ item->setHearts(map.value("hearts").toInt());
+ item->setCategory(map.value("category_name").toString());
+ item->setCompanion(!map.value("companions").toMap().value("android").isNull() || !map.value("companions").toMap().value("ios").isNull());
+
+ QVariantList screenshotsList = map.value("screenshot_images").toList();
+ // try to get more hardware specific screenshots. The store search keeps them in a subgroup.
+ if (map.contains("asset_collections")) {
+ foreach (const QVariant &assetCollection, map.value("asset_collections").toList()) {
+ if (assetCollection.toMap().value("hardware_platform").toString() == m_hardwarePlatform) {
+ screenshotsList = assetCollection.toMap().value("screenshots").toList();
+ break;
+ }
+ }
+ }
+ QStringList screenshotImages;
+ foreach (const QVariant &screenshotItem, screenshotsList) {
+ if (!screenshotItem.toString().isEmpty()) {
+ screenshotImages << screenshotItem.toString();
+ } else if (screenshotItem.toMap().count() > 0) {
+ screenshotImages << screenshotItem.toMap().first().toString();
+ }
+ }
+ item->setScreenshotImages(screenshotImages);
+// qDebug() << "setting screenshot images" << item->screenshotImages();
+
+ // The search seems to return references to invalid icon images. if we detect that, we'll replace it with a screenshot
+ if (item->icon().contains("aOUhkV1R1uCqCVkKY5Dv") && !item->screenshotImages().isEmpty()) {
+ item->setIcon(item->screenshotImages().first());
+ }
+
+ return item;
+}
+
+void AppStoreClient::setBusy(bool busy)
+{
+ m_busy = busy;
+ emit busyChanged();
+}
+
diff --git a/rockwork/appstoreclient.h b/rockwork/appstoreclient.h
new file mode 100644
index 0000000..5a31d60
--- /dev/null
+++ b/rockwork/appstoreclient.h
@@ -0,0 +1,62 @@
+#ifndef APPSTORECLIENT_H
+#define APPSTORECLIENT_H
+
+#include <QObject>
+
+class QNetworkAccessManager;
+class ApplicationsModel;
+class AppItem;
+
+class AppStoreClient : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(Type)
+ Q_PROPERTY(ApplicationsModel* model READ model CONSTANT)
+ Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
+ Q_PROPERTY(QString hardwarePlatform READ hardwarePlatform WRITE setHardwarePlatform NOTIFY hardwarePlatformChanged)
+ Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
+
+public:
+ enum Type {
+ TypeWatchapp,
+ TypeWatchface
+ };
+
+ explicit AppStoreClient(QObject *parent = 0);
+
+ ApplicationsModel *model() const;
+
+ int limit() const;
+ void setLimit(int limit);
+
+ QString hardwarePlatform() const;
+ void setHardwarePlatform(const QString &hardwarePlatform);
+
+ bool busy() const;
+
+signals:
+ void limitChanged();
+ void hardwarePlatformChanged();
+ void busyChanged();
+
+public slots:
+ void fetchHome(Type type);
+ void fetchLink(const QString &link);
+
+ void fetchAppDetails(const QString &appId);
+
+ void search(const QString &searchString, Type type);
+
+private:
+ AppItem *parseAppItem(const QVariantMap &map);
+ void setBusy(bool busy);
+
+private:
+ QNetworkAccessManager *m_nam;
+ ApplicationsModel *m_model;
+ int m_limit = 20;
+ QString m_hardwarePlatform;
+ bool m_busy = false;
+};
+
+#endif // APPSTORECLIENT_H
diff --git a/rockwork/artwork/bianca-black.png b/rockwork/artwork/bianca-black.png
new file mode 100644
index 0000000..d1207c9
--- /dev/null
+++ b/rockwork/artwork/bianca-black.png
Binary files differ
diff --git a/rockwork/artwork/bianca-silver.png b/rockwork/artwork/bianca-silver.png
new file mode 100644
index 0000000..2d003e8
--- /dev/null
+++ b/rockwork/artwork/bianca-silver.png
Binary files differ
diff --git a/rockwork/artwork/black-20mm-hole.png b/rockwork/artwork/black-20mm-hole.png
new file mode 100644
index 0000000..ff61e66
--- /dev/null
+++ b/rockwork/artwork/black-20mm-hole.png
Binary files differ
diff --git a/rockwork/artwork/bobby-black.png b/rockwork/artwork/bobby-black.png
new file mode 100644
index 0000000..83177b5
--- /dev/null
+++ b/rockwork/artwork/bobby-black.png
Binary files differ
diff --git a/rockwork/artwork/bobby-gold.png b/rockwork/artwork/bobby-gold.png
new file mode 100644
index 0000000..d97f2f4
--- /dev/null
+++ b/rockwork/artwork/bobby-gold.png
Binary files differ
diff --git a/rockwork/artwork/bobby-silver.png b/rockwork/artwork/bobby-silver.png
new file mode 100644
index 0000000..44efdf8
--- /dev/null
+++ b/rockwork/artwork/bobby-silver.png
Binary files differ
diff --git a/rockwork/artwork/rockwork.svg b/rockwork/artwork/rockwork.svg
new file mode 100644
index 0000000..e4e92c0
--- /dev/null
+++ b/rockwork/artwork/rockwork.svg
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="72.248886mm"
+ height="72.248886mm"
+ viewBox="0 0 255.99999 255.99999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="upebble.svg">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ style="color-interpolation-filters:sRGB"
+ id="filter4248"
+ x="-0.025328101"
+ width="1.0506562"
+ y="-0.013960773"
+ height="1.0279215">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.3907822"
+ id="feGaussianBlur4250" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="89.121544"
+ inkscape:cy="77.044911"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2880"
+ inkscape:window-height="1752"
+ inkscape:window-x="0"
+ inkscape:window-y="48"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136"
+ originx="-40.000001"
+ originy="-539"
+ snapvisiblegridlinesonly="true"
+ enabled="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-40,-257.36221)">
+ <rect
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4200"
+ width="256"
+ height="256"
+ x="40"
+ y="257.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:0.0479798;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 40,257.36221 256,0 -256,256 z"
+ id="rect4252"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <g
+ id="g4300">
+ <g
+ style="fill:#000000;fill-opacity:1;opacity:0.291;filter:url(#filter4248)"
+ id="g4202"
+ transform="matrix(0.60632857,0,0,0.60632857,-37.462675,74.399202)">
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="path4204"
+ d="m 437.97969,445.08937 c 0,0 11.49464,-4.59544 12.27285,0.25253 0.77821,4.84797 2.06459,45.23266 2.06459,45.23266 -8.36034,0.32794 -13.15013,-0.0886 -13.15013,-0.0886 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path4206"
+ d="m 439.49492,491.6046 12.68287,0.70015 0.54937,42.27954 -13.73731,0.25254 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4208"
+ width="120"
+ height="95"
+ x="280"
+ y="623.36218" />
+ <rect
+ y="307.36221"
+ x="280"
+ height="95"
+ width="120"
+ id="rect4210"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="path4212"
+ d="M 228.0862,442.4309 C 228.58744,435.98794 240,437.36221 240,437.36221 l 0,42.02031 c 0,0 -14.31567,-1.22669 -13.80125,-2.84014 0.51442,-1.61345 1.3862,-27.66851 1.88745,-34.11148 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 420,643.715 337.60905,658.36221 255,643.715 l 0,-20 165,0 z"
+ id="path4214"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4216"
+ d="M 255,382.36221 337.39095,367.715 420,382.36221 l 0,20 -165,0 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="path4218"
+ d="m 438.52906,535.82255 c 0,0 5.15979,0.84007 13.83236,0.44761 0.13423,13.76866 -1.20901,37.74804 -1.85634,42.35471 -0.64733,4.60667 -11.01016,-0.50508 -11.01016,-0.50508 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="path4220"
+ d="m 244.85206,406.56127 c 16.07143,-13.92858 66.12644,-13.34299 97.65304,-13.30725 31.5266,0.0357 73.56632,-0.53467 90.70918,15.17961 17.14286,15.71428 12.91706,70.98675 13.01566,106.0726 0.0986,35.08586 5.19864,81.42741 -13.01565,99.64169 C 415,632.3622 371.12033,628.47664 339.15317,628.18658 307.186,627.89652 263.91063,632.7014 245.3392,615.91569 226.76777,599.12997 231.43107,540.45867 231.61582,505.9352 c 0.18475,-34.52347 -2.83519,-85.44536 13.23624,-99.37393 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ ry="20"
+ rx="20"
+ y="422.36221"
+ x="260"
+ height="174.99998"
+ width="155"
+ id="rect4222"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ y="442.36221"
+ x="280"
+ height="135"
+ width="120"
+ id="rect4224"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="path4226"
+ d="m 375,442.36221 25,0 0,135 -25,0 0,-109.75206 -7.32361,-5.3033 7.32361,-4.9245 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="rect4177"
+ d="m 228.09692,344.2696 c 0,0 6.96953,-2.78634 7.44138,0.15312 0.47185,2.93946 1.25182,27.42585 1.25182,27.42585 -5.06911,0.19884 -7.9733,-0.0537 -7.9733,-0.0537 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="rect4179"
+ d="m 229.01565,372.47312 7.68999,0.42452 0.3331,25.63529 -8.32933,0.15312 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4167"
+ width="72.75943"
+ height="57.601215"
+ x="132.30933"
+ y="452.36151" />
+ <rect
+ y="260.76169"
+ x="132.30933"
+ height="57.601215"
+ width="72.75943"
+ id="rect4165"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="rect4169"
+ d="m 100.8325,342.6577 c 0.30392,-3.90655 7.22368,-3.07329 7.22368,-3.07329 l 0,25.47811 c 0,0 -8.679998,-0.74378 -8.36809,-1.72206 0.311907,-0.97828 0.84049,-16.77621 1.14441,-20.68276 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 217.19532,464.702 -49.95598,8.88102 -50.08823,-8.88102 0,-12.12657 100.04421,0 z"
+ id="path4175"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect4172"
+ d="m 117.15111,306.23633 49.95599,-8.88102 50.08822,8.88102 0,12.12658 -100.04421,0 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="rect4181"
+ d="m 228.43002,399.28372 c 0,0 3.12853,0.50936 8.38696,0.2714 0.0814,8.34833 -0.73306,22.88772 -1.12555,25.68087 -0.3925,2.79316 -6.67578,-0.30624 -6.67578,-0.30624 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="rect4149"
+ d="m 110.99812,320.90892 c 9.74457,-8.4453 40.09435,-8.09024 59.20983,-8.06857 19.11548,0.0217 44.60536,-0.32419 54.99957,9.20383 10.39421,9.52802 7.83198,43.0413 7.89177,64.31485 0.0598,21.27356 3.15208,49.37176 -7.89176,60.4156 -11.04385,11.04384 -37.64935,8.68791 -57.03195,8.51204 -19.38261,-0.17587 -45.6217,2.73747 -56.88209,-7.44019 -11.26039,-10.17766 -8.4329,-45.75175 -8.32088,-66.68431 0.11202,-20.93257 -1.71905,-51.80796 8.02551,-60.25325 z"
+ style="opacity:1;fill:#cbcbcb;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 133.22587,330.48948 69.72779,0 c 6.71812,0 11.83367,5.41484 12.12657,12.12657 1.19073,27.28478 1.19022,54.56956 0,81.85435 -0.29278,6.71174 -5.40845,12.12657 -12.12657,12.12657 l -69.72779,0 c -6.71812,0 -11.82926,-5.41504 -12.12657,-12.12657 -1.18246,-26.69356 -1.65764,-53.74075 0,-81.85435 0.39543,-6.70647 5.40845,-12.12657 12.12657,-12.12657 z"
+ id="rect4152"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssssss" />
+ <rect
+ y="342.61606"
+ x="131.62029"
+ height="81.854355"
+ width="72.75943"
+ id="rect4154"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="rect4156"
+ d="m 189.2215,342.61605 15.15821,0 0,81.85436 -15.15821,0 0,-66.54581 -4.44052,-3.21555 4.44052,-2.98586 z"
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ style="fill:#000000"
+ d="m 143.63128,393.17695 c -0.82884,-0.82884 0.32908,-1.46136 2.67524,-1.46136 3.02637,0 3.07586,-0.7116 0.31911,-4.58902 l -2.10477,-2.96041 2.0287,-2.94922 c 1.15063,-1.67272 2.20678,-4.13671 2.44014,-5.69282 0.40696,-2.71383 0.44252,-2.74828 3.27625,-3.17322 1.57564,-0.23629 4.03472,-1.23453 5.46462,-2.21832 1.42988,-0.98379 2.85437,-1.78871 3.16551,-1.78871 0.31114,0 1.68177,0.80847 3.04585,1.7966 1.40341,1.01663 3.70432,1.98019 5.29992,2.21946 2.92933,0.43928 2.99923,0.52347 3.81465,4.59457 0.23714,1.18397 1.22649,3.16119 2.19857,4.39382 0.97208,1.23265 1.76741,2.42825 1.76741,2.6569 0,0.22864 -0.83031,1.62253 -1.84514,3.09753 -2.44659,3.556 -2.38325,4.49115 0.32038,4.72947 1.19102,0.10499 2.22891,0.46766 2.30641,0.80594 0.15778,0.68864 -33.49245,1.21919 -34.17285,0.53879 z m 27.8729,-2.84521 c 0.009,-0.76112 0.83738,-2.4679 1.83992,-3.79283 l 1.82279,-2.40898 -1.58789,-2.08183 c -0.87334,-1.14501 -1.90888,-3.37431 -2.30119,-4.954 -0.68475,-2.75721 -0.82407,-2.89364 -3.48101,-3.40876 -1.52223,-0.29514 -3.68339,-1.22898 -4.80255,-2.0752 l -2.03486,-1.53859 -2.27046,1.53481 c -2.19134,1.48132 -2.85113,1.74296 -6.34208,2.51493 -1.32103,0.29212 -1.68348,0.80695 -2.01126,2.85674 -0.2199,1.37522 -1.11243,3.57721 -1.98338,4.8933 l -1.58355,2.39291 1.54803,2.0343 c 0.85141,1.11887 1.73796,2.79542 1.97011,3.72567 l 0.42209,1.69138 10.38909,0 c 10.35877,0 10.38914,-0.004 10.4062,-1.38385 z m -16.16204,-1.17442 c -3.00082,-0.83158 -3.29439,-2.70542 -0.30753,-1.96294 1.0994,0.27329 2.9676,0.49689 4.15156,0.49689 1.18396,0 3.05216,-0.2236 4.15155,-0.49689 1.34833,-0.33517 1.9989,-0.27586 1.9989,0.18225 0,1.61808 -6.44052,2.76557 -9.99448,1.78069 z m -1.87921,-5.21512 c -0.23497,-0.23497 -0.42721,-1.51123 -0.42721,-2.83614 0,-2.76084 1.40937,-2.98541 1.73071,-0.27578 0.21201,1.78766 -0.62633,3.78908 -1.3035,3.11192 z m 10.02856,-2.68238 c 0,-1.7768 0.25627,-2.46018 0.92256,-2.46018 0.6663,0 0.92257,0.68338 0.92257,2.46018 0,1.7768 -0.25627,2.46018 -0.92257,2.46018 -0.66629,0 -0.92256,-0.68338 -0.92256,-2.46018 z m -22.75668,2.7677 c 0,-0.69366 0.43166,-0.94682 1.38386,-0.81158 0.76111,0.10809 1.38385,0.47331 1.38385,0.81158 0,0.33828 -0.62274,0.70349 -1.38385,0.81159 -0.9522,0.13523 -1.38386,-0.11793 -1.38386,-0.81159 z m 37.8226,0.30318 c -0.46699,-0.7556 1.22568,-1.49279 2.13592,-0.93022 0.35999,0.22248 0.4953,0.66216 0.30068,0.97707 -0.46943,0.75955 -1.95589,0.73096 -2.4366,-0.0469 z m -32.49939,-13.42132 c -1.23387,-1.36341 -1.29125,-1.95046 -0.19065,-1.95046 1.05106,0 3.00139,2.11681 2.45435,2.66385 -0.65422,0.65422 -1.17438,0.49029 -2.2637,-0.71339 z m 27.38286,0.11435 c 0.23773,-1.20797 2.11252,-2.50883 2.72827,-1.89307 0.48678,0.48678 -1.47947,2.90348 -2.36232,2.90348 -0.31065,0 -0.47533,-0.45468 -0.36595,-1.01041 z m -13.38512,-4.7488 c -0.52662,-1.37234 -0.0519,-3.6431 0.82178,-3.93083 0.51752,-0.17043 0.76881,0.45533 0.76881,1.91443 0,2.29458 -0.99824,3.56005 -1.59059,2.0164 z"
+ id="path4285"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssscsssssssssssssssscsssscsssscsscsssssssssssssssssssssscssccssssssssssss" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="136.46957"
+ y="405.82156"
+ id="text4342"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4344"
+ x="136.46957"
+ y="405.82156"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;font-family:Ubuntu;-inkscape-font-specification:Ubuntu">Tomorrow</tspan></text>
+ </g>
+</svg>
diff --git a/rockwork/artwork/snowy-black.png b/rockwork/artwork/snowy-black.png
new file mode 100644
index 0000000..acf3439
--- /dev/null
+++ b/rockwork/artwork/snowy-black.png
Binary files differ
diff --git a/rockwork/artwork/snowy-red.png b/rockwork/artwork/snowy-red.png
new file mode 100644
index 0000000..b0bdc8e
--- /dev/null
+++ b/rockwork/artwork/snowy-red.png
Binary files differ
diff --git a/rockwork/artwork/snowy-white.png b/rockwork/artwork/snowy-white.png
new file mode 100644
index 0000000..3bfe6d1
--- /dev/null
+++ b/rockwork/artwork/snowy-white.png
Binary files differ
diff --git a/rockwork/artwork/spalding-14mm-black.png b/rockwork/artwork/spalding-14mm-black.png
new file mode 100644
index 0000000..47b5b03
--- /dev/null
+++ b/rockwork/artwork/spalding-14mm-black.png
Binary files differ
diff --git a/rockwork/artwork/spalding-14mm-rose-gold.png b/rockwork/artwork/spalding-14mm-rose-gold.png
new file mode 100644
index 0000000..8775cf1
--- /dev/null
+++ b/rockwork/artwork/spalding-14mm-rose-gold.png
Binary files differ
diff --git a/rockwork/artwork/spalding-14mm-silver.png b/rockwork/artwork/spalding-14mm-silver.png
new file mode 100644
index 0000000..bcc5f16
--- /dev/null
+++ b/rockwork/artwork/spalding-14mm-silver.png
Binary files differ
diff --git a/rockwork/artwork/spalding-20mm-black.png b/rockwork/artwork/spalding-20mm-black.png
new file mode 100644
index 0000000..d00a1f7
--- /dev/null
+++ b/rockwork/artwork/spalding-20mm-black.png
Binary files differ
diff --git a/rockwork/artwork/spalding-20mm-silver.png b/rockwork/artwork/spalding-20mm-silver.png
new file mode 100644
index 0000000..18b0e02
--- /dev/null
+++ b/rockwork/artwork/spalding-20mm-silver.png
Binary files differ
diff --git a/rockwork/artwork/tintin-black.png b/rockwork/artwork/tintin-black.png
new file mode 100644
index 0000000..dcf2c31
--- /dev/null
+++ b/rockwork/artwork/tintin-black.png
Binary files differ
diff --git a/rockwork/artwork/tintin-blue.png b/rockwork/artwork/tintin-blue.png
new file mode 100644
index 0000000..eca2d3b
--- /dev/null
+++ b/rockwork/artwork/tintin-blue.png
Binary files differ
diff --git a/rockwork/artwork/tintin-green.png b/rockwork/artwork/tintin-green.png
new file mode 100644
index 0000000..17df060
--- /dev/null
+++ b/rockwork/artwork/tintin-green.png
Binary files differ
diff --git a/rockwork/artwork/tintin-grey.png b/rockwork/artwork/tintin-grey.png
new file mode 100644
index 0000000..4f9988b
--- /dev/null
+++ b/rockwork/artwork/tintin-grey.png
Binary files differ
diff --git a/rockwork/artwork/tintin-orange.png b/rockwork/artwork/tintin-orange.png
new file mode 100644
index 0000000..5956126
--- /dev/null
+++ b/rockwork/artwork/tintin-orange.png
Binary files differ
diff --git a/rockwork/artwork/tintin-pink.png b/rockwork/artwork/tintin-pink.png
new file mode 100644
index 0000000..ee69d67
--- /dev/null
+++ b/rockwork/artwork/tintin-pink.png
Binary files differ
diff --git a/rockwork/artwork/tintin-red.png b/rockwork/artwork/tintin-red.png
new file mode 100644
index 0000000..6c7b7e2
--- /dev/null
+++ b/rockwork/artwork/tintin-red.png
Binary files differ
diff --git a/rockwork/artwork/tintin-white.png b/rockwork/artwork/tintin-white.png
new file mode 100644
index 0000000..912ea19
--- /dev/null
+++ b/rockwork/artwork/tintin-white.png
Binary files differ
diff --git a/rockwork/main.cpp b/rockwork/main.cpp
new file mode 100644
index 0000000..70fd0d7
--- /dev/null
+++ b/rockwork/main.cpp
@@ -0,0 +1,37 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQuickView>
+#include <QtQml>
+#include <QFile>
+
+#include "notificationsourcemodel.h"
+#include "servicecontrol.h"
+#include "pebbles.h"
+#include "pebble.h"
+#include "applicationsmodel.h"
+#include "applicationsfiltermodel.h"
+#include "appstoreclient.h"
+#include "screenshotmodel.h"
+
+int main(int argc, char *argv[])
+{
+ QGuiApplication app(argc, argv);
+
+ qmlRegisterUncreatableType<Pebble>("RockWork", 1, 0, "Pebble", "Get them from the model");
+ qmlRegisterUncreatableType<ApplicationsModel>("RockWork", 1, 0, "ApplicationsModel", "Get them from a Pebble object");
+ qmlRegisterUncreatableType<AppItem>("RockWork", 1, 0, "AppItem", "Get them from an ApplicationsModel");
+ qmlRegisterType<ApplicationsFilterModel>("RockWork", 1, 0, "ApplicationsFilterModel");
+ qmlRegisterType<Pebbles>("RockWork", 1, 0, "Pebbles");
+ qmlRegisterUncreatableType<NotificationSourceModel>("RockWork", 1, 0, "NotificationSourceModel", "Get it from a Pebble object");
+ qmlRegisterType<ServiceControl>("RockWork", 1, 0, "ServiceController");
+ qmlRegisterType<AppStoreClient>("RockWork", 1, 0, "AppStoreClient");
+ qmlRegisterType<ScreenshotModel>("RockWork", 1, 0, "ScreenshotModel");
+
+ QQuickView view;
+ view.engine()->rootContext()->setContextProperty("version", QStringLiteral(VERSION));
+ view.engine()->rootContext()->setContextProperty("homePath", QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first());
+ view.setSource(QUrl(QStringLiteral("qrc:///Main.qml")));
+ view.setResizeMode(QQuickView::SizeRootObjectToView);
+ view.show();
+ return app.exec();
+}
diff --git a/rockwork/notificationsourcemodel.cpp b/rockwork/notificationsourcemodel.cpp
new file mode 100644
index 0000000..cbb75ca
--- /dev/null
+++ b/rockwork/notificationsourcemodel.cpp
@@ -0,0 +1,117 @@
+#include "notificationsourcemodel.h"
+
+#include <QSettings>
+#include <QDebug>
+
+NotificationSourceModel::NotificationSourceModel(QObject *parent) : QAbstractListModel(parent)
+{
+}
+
+int NotificationSourceModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_sources.count();
+}
+
+QVariant NotificationSourceModel::data(const QModelIndex &index, int role) const
+{
+ NotificationSourceItem item = m_sources.at(index.row());
+ switch (role) {
+ case RoleName:
+ return item.m_displayName;
+ case RoleEnabled:
+ return item.m_enabled;
+ case RoleIcon:
+ return item.m_icon;
+ }
+ return QVariant();
+}
+
+QHash<int, QByteArray> NotificationSourceModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles.insert(RoleName, "name");
+ roles.insert(RoleEnabled, "enabled");
+ roles.insert(RoleIcon, "icon");
+ return roles;
+}
+
+void NotificationSourceModel::insert(const QString &sourceId, bool enabled)
+{
+ qDebug() << "changed" << sourceId << enabled;
+
+ int idx = -1;
+ for (int i = 0; i < m_sources.count(); i++) {
+ if (m_sources.at(i).m_id == sourceId) {
+ idx = i;
+ }
+ }
+
+ if (idx >= 0) {
+ m_sources[idx].m_enabled = enabled;
+ emit dataChanged(index(idx), index(idx), {RoleEnabled});
+ } else {
+ beginInsertRows(QModelIndex(), m_sources.count(), m_sources.count());
+ NotificationSourceItem item = fromDesktopFile(sourceId);
+ item.m_enabled = enabled;
+ m_sources.append(item);
+ endInsertRows();
+ }
+}
+
+#include <QStandardPaths>
+#include <QFileInfo>
+#include <QDir>
+
+NotificationSourceItem NotificationSourceModel::fromDesktopFile(const QString &sourceId)
+{
+ NotificationSourceItem ret;
+ ret.m_id = sourceId;
+ ret.m_icon = "dialog-question-symbolic";
+
+ QString desktopFilePath;
+ QStringList appsDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
+ foreach (const QString &appsDir, appsDirs) {
+ QDir dir(appsDir);
+ QFileInfoList entries = dir.entryInfoList({sourceId + "*.desktop"});
+ if (entries.count() > 0) {
+ desktopFilePath = entries.first().absoluteFilePath();
+ break;
+ }
+ }
+
+ if (desktopFilePath.isEmpty()) {
+ // Lets see if this is an indicator
+ QDir dir("/usr/share/upstart/xdg/autostart/");
+ QString serviceName = sourceId;
+ serviceName.remove("-service");
+ QFileInfoList entries = dir.entryInfoList({serviceName + "*.desktop"});
+ if (entries.count() > 0) {
+ desktopFilePath = entries.first().absoluteFilePath();
+ if (sourceId == "indicator-power-service") {
+ ret.m_icon = "gpm-battery-050";
+ } else if (sourceId == "indicator-datetime-service") {
+ ret.m_icon = "alarm-clock";
+ } else {
+ ret.m_icon = "settings";
+ }
+ }
+ }
+
+ if (desktopFilePath.isEmpty()) {
+ qWarning() << ".desktop file not found for" << sourceId;
+ ret.m_displayName = sourceId;
+ return ret;
+ }
+
+ QSettings s(desktopFilePath, QSettings::IniFormat);
+ s.beginGroup("Desktop Entry");
+ ret.m_displayName = s.value("Name").toString();
+ if (!s.value("Icon").toString().isEmpty()) {
+ ret.m_icon = s.value("Icon").toString();
+ }
+
+ qDebug() << "parsed file:" << desktopFilePath << ret.m_displayName << ret.m_icon;
+ return ret;
+}
+
diff --git a/rockwork/notificationsourcemodel.h b/rockwork/notificationsourcemodel.h
new file mode 100644
index 0000000..89fa26f
--- /dev/null
+++ b/rockwork/notificationsourcemodel.h
@@ -0,0 +1,48 @@
+#ifndef NOTIFICATIONSOURCEMODEL_H
+#define NOTIFICATIONSOURCEMODEL_H
+
+#include <QAbstractListModel>
+
+class NotificationSourceItem
+{
+public:
+ QString m_id;
+ QString m_displayName;
+ QString m_icon;
+ bool m_enabled = false;
+
+ bool operator ==(const NotificationSourceItem &other) {
+ return m_id == other.m_id;
+ }
+};
+
+class NotificationSourceModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+public:
+ enum Roles {
+ RoleName,
+ RoleEnabled,
+ RoleIcon
+ };
+
+ explicit NotificationSourceModel(QObject *parent = 0);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ void insert(const QString &sourceId, bool enabled);
+
+signals:
+ void countChanged();
+
+private:
+ NotificationSourceItem fromDesktopFile(const QString &sourceId);
+
+private:
+ QList<NotificationSourceItem> m_sources;
+};
+
+#endif // NOTIFICATIONSOURCEMODEL_H
diff --git a/rockwork/org.freedesktop.Notifications.xml b/rockwork/org.freedesktop.Notifications.xml
new file mode 100644
index 0000000..b694a55
--- /dev/null
+++ b/rockwork/org.freedesktop.Notifications.xml
@@ -0,0 +1,45 @@
+<!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.freedesktop.Notifications">
+ <!-- Desktop Notification Specification interface -->
+ <method name="GetCapabilities">
+ <arg name="capabilities" type="as" direction="out"/>
+ </method>
+ <method name="Notify">
+ <arg name="app_name" type="s" direction="in"/>
+ <arg name="replaces_id" type="u" direction="in"/>
+ <arg name="app_icon" type="s" direction="in"/>
+ <arg name="summary" type="s" direction="in"/>
+ <arg name="body" type="s" direction="in"/>
+ <arg name="actions" type="as" direction="in"/>
+ <arg name="hints" type="a{sv}" direction="in"/>
+ <arg name="expire_timeout" type="i" direction="in"/>
+ <arg name="id" type="u" direction="out"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
+ </method>
+ <method name="CloseNotification">
+ <arg name="id" type="u" direction="in"/>
+ </method>
+ <method name="GetServerInformation">
+ <arg name="name" type="s" direction="out"/>
+ <arg name="vendor" type="s" direction="out"/>
+ <arg name="version" type="s" direction="out"/>
+ <arg name="specVersion" type="s" direction="out"/>
+ </method>
+ <signal name="NotificationClosed">
+ <arg name="id" type="u"/>
+ <arg name="reason" type="u"/>
+ </signal>
+ <signal name="ActionInvoked">
+ <arg name="id" type="u"/>
+ <arg name="action_key" type="s"/>
+ </signal>
+
+ <!-- Extra method to enable testing -->
+ <method name="GetNotifications">
+ <arg name="app_name" type="s" direction="in"/>
+ <arg name="notifications" type="a(sussasa{sv}i)" direction="out"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="NotificationDataList"/>
+ </method>
+ </interface>
+</node>
diff --git a/rockwork/pebble.cpp b/rockwork/pebble.cpp
new file mode 100644
index 0000000..cba97d3
--- /dev/null
+++ b/rockwork/pebble.cpp
@@ -0,0 +1,432 @@
+#include "pebble.h"
+#include "notificationsourcemodel.h"
+#include "applicationsmodel.h"
+#include "screenshotmodel.h"
+
+#include <QDBusArgument>
+#include <QDebug>
+
+Pebble::Pebble(const QDBusObjectPath &path, QObject *parent):
+ QObject(parent),
+ m_path(path)
+{
+ m_iface = new QDBusInterface("org.rockwork", path.path(), "org.rockwork.Pebble", QDBusConnection::sessionBus(), this);
+ m_notifications = new NotificationSourceModel(this);
+ m_installedApps = new ApplicationsModel(this);
+ connect(m_installedApps, &ApplicationsModel::appsSorted, this, &Pebble::appsSorted);
+ m_installedWatchfaces = new ApplicationsModel(this);
+ m_screenshotModel = new ScreenshotModel(this);
+
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "Connected", this, SLOT(pebbleConnected()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "Disconnected", this, SLOT(pebbleDisconnected()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "InstalledAppsChanged", this, SLOT(refreshApps()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "OpenURL", this, SIGNAL(openURL(const QString&, const QString&)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "NotificationFilterChanged", this, SLOT(notificationFilterChanged(const QString &, bool)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "ScreenshotAdded", this, SLOT(screenshotAdded(const QString &)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "ScreenshotRemoved", this, SLOT(screenshotRemoved(const QString &)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "FirmwareUpgradeAvailableChanged", this, SLOT(refreshFirmwareUpdateInfo()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "UpgradingFirmwareChanged", this, SIGNAL(refreshFirmwareUpdateInfo()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "LogsDumped", this, SIGNAL(logsDumped(bool)));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "HealthParamsChanged", this, SIGNAL(healthParamsChanged()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "ImperialUnitsChanged", this, SIGNAL(imperialUnitsChanged()));
+ QDBusConnection::sessionBus().connect("org.rockwork", path.path(), "org.rockwork.Pebble", "CalendarSyncEnabledChanged", this, SIGNAL(calendarSyncEnabledChanged()));
+
+ dataChanged();
+ refreshApps();
+ refreshNotifications();
+ refreshScreenshots();
+ refreshFirmwareUpdateInfo();
+}
+
+bool Pebble::connected() const
+{
+ return m_connected;
+}
+
+QDBusObjectPath Pebble::path()
+{
+ return m_path;
+}
+
+QString Pebble::address() const
+{
+ return m_address;
+}
+
+QString Pebble::name() const
+{
+ return m_name;
+}
+
+QString Pebble::hardwarePlatform() const
+{
+ return m_hardwarePlatform;
+}
+
+QString Pebble::serialNumber() const
+{
+ return m_serialNumber;
+}
+
+QString Pebble::softwareVersion() const
+{
+ return m_softwareVersion;
+}
+
+int Pebble::model() const
+{
+ return m_model;
+}
+
+bool Pebble::recovery() const
+{
+ return m_recovery;
+}
+
+bool Pebble::upgradingFirmware() const
+{
+ qDebug() << "upgrading firmware" << m_upgradingFirmware;
+ return m_upgradingFirmware;
+}
+
+NotificationSourceModel *Pebble::notifications() const
+{
+ return m_notifications;
+}
+
+ApplicationsModel *Pebble::installedApps() const
+{
+ return m_installedApps;
+}
+
+ApplicationsModel *Pebble::installedWatchfaces() const
+{
+ return m_installedWatchfaces;
+}
+
+ScreenshotModel *Pebble::screenshots() const
+{
+ return m_screenshotModel;
+}
+
+bool Pebble::firmwareUpgradeAvailable() const
+{
+ return m_firmwareUpgradeAvailable;
+}
+
+QString Pebble::firmwareReleaseNotes() const
+{
+ return m_firmwareReleaseNotes;
+}
+
+QString Pebble::candidateVersion() const
+{
+ return m_candidateVersion;
+}
+
+QVariantMap Pebble::healthParams() const
+{
+ QDBusMessage m = m_iface->call("HealthParams");
+ if (m.type() == QDBusMessage::ErrorMessage || m.arguments().count() == 0) {
+ qWarning() << "Could not fetch health params" << m.errorMessage();
+ return QVariantMap();
+ }
+
+ const QDBusArgument &arg = m.arguments().first().value<QDBusArgument>();
+
+ QVariantMap mapEntryVariant;
+ arg >> mapEntryVariant;
+
+ qDebug() << "have health params" << mapEntryVariant;
+ return mapEntryVariant;
+}
+
+void Pebble::setHealthParams(const QVariantMap &healthParams)
+{
+ m_iface->call("SetHealthParams", healthParams);
+}
+
+bool Pebble::imperialUnits() const
+{
+ return fetchProperty("ImperialUnits").toBool();
+}
+
+void Pebble::setImperialUnits(bool imperialUnits)
+{
+ qDebug() << "setting im units" << imperialUnits;
+ m_iface->call("SetImperialUnits", imperialUnits);
+}
+
+bool Pebble::calendarSyncEnabled() const
+{
+ return fetchProperty("CalendarSyncEnabled").toBool();
+}
+
+void Pebble::setCalendarSyncEnabled(bool enabled)
+{
+ m_iface->call("SetCalendarSyncEnabled", enabled);
+}
+
+void Pebble::configurationClosed(const QString &uuid, const QString &url)
+{
+ m_iface->call("ConfigurationClosed", uuid, url.mid(17));
+}
+
+void Pebble::launchApp(const QString &uuid)
+{
+ m_iface->call("LaunchApp", uuid);
+}
+
+void Pebble::requestConfigurationURL(const QString &uuid)
+{
+ m_iface->call("ConfigurationURL", uuid);
+}
+
+void Pebble::removeApp(const QString &uuid)
+{
+ qDebug() << "should remove app" << uuid;
+ m_iface->call("RemoveApp", uuid);
+}
+
+void Pebble::installApp(const QString &storeId)
+{
+ qDebug() << "should install app" << storeId;
+ m_iface->call("InstallApp", storeId);
+}
+
+void Pebble::sideloadApp(const QString &packageFile)
+{
+ m_iface->call("SideloadApp", packageFile);
+}
+
+QVariant Pebble::fetchProperty(const QString &propertyName) const
+{
+ QDBusMessage m = m_iface->call(propertyName);
+ if (m.type() != QDBusMessage::ErrorMessage && m.arguments().count() == 1) {
+ qDebug() << "property" << propertyName << m.arguments().first();
+ return m.arguments().first();
+
+ }
+ qDebug() << "error getting property:" << propertyName << m.errorMessage();
+ return QVariant();
+}
+
+void Pebble::dataChanged()
+{
+ qDebug() << "data changed";
+ m_name = fetchProperty("Name").toString();
+ m_address = fetchProperty("Address").toString();
+ m_serialNumber = fetchProperty("SerialNumber").toString();
+ m_serialNumber = fetchProperty("SerialNumber").toString();
+ QString hardwarePlatform = fetchProperty("HardwarePlatform").toString();
+ if (hardwarePlatform != m_hardwarePlatform) {
+ m_hardwarePlatform = hardwarePlatform;
+ emit hardwarePlatformChanged();
+ }
+ m_softwareVersion = fetchProperty("SoftwareVersion").toString();
+ m_model = fetchProperty("Model").toInt();
+ m_recovery = fetchProperty("Recovery").toBool();
+ qDebug() << "model is" << m_model;
+ emit modelChanged();
+
+ bool connected = fetchProperty("IsConnected").toBool();
+ if (connected != m_connected) {
+ m_connected = connected;
+ emit connectedChanged();
+ }
+}
+
+void Pebble::pebbleConnected()
+{
+
+ dataChanged();
+ m_connected = true;
+ emit connectedChanged();
+
+ refreshApps();
+ refreshNotifications();
+ refreshScreenshots();
+}
+
+void Pebble::pebbleDisconnected()
+{
+ m_connected = false;
+ emit connectedChanged();
+}
+
+void Pebble::notificationFilterChanged(const QString &sourceId, bool enabled)
+{
+ m_notifications->insert(sourceId, enabled);
+}
+
+void Pebble::refreshNotifications()
+{
+ QDBusMessage m = m_iface->call("NotificationsFilter");
+ if (m.type() == QDBusMessage::ErrorMessage || m.arguments().count() == 0) {
+ qWarning() << "Could not fetch notifications filter" << m.errorMessage();
+ return;
+ }
+
+ const QDBusArgument &arg = m.arguments().first().value<QDBusArgument>();
+
+ QVariantMap mapEntryVariant;
+ arg >> mapEntryVariant;
+
+ foreach (const QString &sourceId, mapEntryVariant.keys()) {
+ m_notifications->insert(sourceId, mapEntryVariant.value(sourceId).toBool());
+ }
+}
+
+void Pebble::setNotificationFilter(const QString &sourceId, bool enabled)
+{
+ m_iface->call("SetNotificationFilter", sourceId, enabled);
+}
+
+void Pebble::moveApp(const QString &uuid, int toIndex)
+{
+ // This is a bit tricky:
+ AppItem *item = m_installedApps->findByUuid(uuid);
+ if (!item) {
+ qWarning() << "item not found";
+ return;
+ }
+ int realToIndex = 0;
+ for (int i = 0; i < m_installedApps->rowCount(); i++) {
+ if (item->isWatchFace() && m_installedApps->get(i)->isWatchFace()) {
+ realToIndex++;
+ } else if (!item->isWatchFace() && !m_installedApps->get(i)->isWatchFace()) {
+ realToIndex++;
+ }
+ if (realToIndex == toIndex) {
+ realToIndex = i+1;
+ break;
+ }
+ }
+ m_iface->call("MoveApp", m_installedApps->indexOf(item), realToIndex);
+}
+
+void Pebble::refreshApps()
+{
+
+ QDBusMessage m = m_iface->call("InstalledApps");
+ if (m.type() == QDBusMessage::ErrorMessage || m.arguments().count() == 0) {
+ qWarning() << "Could not fetch installed apps" << m.errorMessage();
+ return;
+ }
+
+ m_installedApps->clear();
+ m_installedWatchfaces->clear();
+
+ const QDBusArgument &arg = m.arguments().first().value<QDBusArgument>();
+
+ QVariantList appList;
+
+ arg.beginArray();
+ while (!arg.atEnd()) {
+ QVariant mapEntryVariant;
+ arg >> mapEntryVariant;
+
+ QDBusArgument mapEntry = mapEntryVariant.value<QDBusArgument>();
+ QVariantMap appMap;
+ mapEntry >> appMap;
+ appList.append(appMap);
+
+ }
+ arg.endArray();
+
+
+ qDebug() << "have apps" << appList;
+ foreach (const QVariant &v, appList) {
+ AppItem *app = new AppItem(this);
+ app->setStoreId(v.toMap().value("storeId").toString());
+ app->setUuid(v.toMap().value("uuid").toString());
+ app->setName(v.toMap().value("name").toString());
+ app->setIcon(v.toMap().value("icon").toString());
+ app->setVendor(v.toMap().value("vendor").toString());
+ app->setVersion(v.toMap().value("version").toString());
+ app->setIsWatchFace(v.toMap().value("watchface").toBool());
+ app->setHasSettings(v.toMap().value("hasSettings").toBool());
+ app->setIsSystemApp(v.toMap().value("systemApp").toBool());
+
+ if (app->isWatchFace()) {
+ m_installedWatchfaces->insert(app);
+ } else {
+ m_installedApps->insert(app);
+ }
+ }
+}
+
+void Pebble::appsSorted()
+{
+ QStringList newList;
+ for (int i = 0; i < m_installedApps->rowCount(); i++) {
+ newList << m_installedApps->get(i)->uuid();
+ }
+ for (int i = 0; i < m_installedWatchfaces->rowCount(); i++) {
+ newList << m_installedWatchfaces->get(i)->uuid();
+ }
+ m_iface->call("SetAppOrder", newList);
+}
+
+void Pebble::refreshScreenshots()
+{
+ m_screenshotModel->clear();
+ QStringList screenshots = fetchProperty("Screenshots").toStringList();
+ foreach (const QString &filename, screenshots) {
+ m_screenshotModel->insert(filename);
+ }
+}
+
+void Pebble::screenshotAdded(const QString &filename)
+{
+ qDebug() << "screenshot added" << filename;
+ m_screenshotModel->insert(filename);
+}
+
+void Pebble::screenshotRemoved(const QString &filename)
+{
+ m_screenshotModel->remove(filename);
+}
+
+void Pebble::refreshFirmwareUpdateInfo()
+{
+ bool firmwareUpgradeAvailable = fetchProperty("FirmwareUpgradeAvailable").toBool();
+ if (firmwareUpgradeAvailable && !m_firmwareUpgradeAvailable) {
+ m_firmwareUpgradeAvailable = true;
+ m_firmwareReleaseNotes = fetchProperty("FirmwareReleaseNotes").toString();
+ m_candidateVersion = fetchProperty("CandidateFirmwareVersion").toString();
+ qDebug() << "firmare upgrade" << m_firmwareUpgradeAvailable << m_firmwareReleaseNotes << m_candidateVersion;
+ emit firmwareUpgradeAvailableChanged();
+ } else if (!firmwareUpgradeAvailable && m_firmwareUpgradeAvailable) {
+ m_firmwareUpgradeAvailable = false;
+ m_firmwareReleaseNotes.clear();;
+ m_candidateVersion.clear();
+ emit firmwareUpgradeAvailableChanged();
+ }
+ bool upgradingFirmware = fetchProperty("UpgradingFirmware").toBool();
+ if (m_upgradingFirmware != upgradingFirmware) {
+ m_upgradingFirmware = upgradingFirmware;
+ emit upgradingFirmwareChanged();
+ }
+}
+
+void Pebble::requestScreenshot()
+{
+ m_iface->call("RequestScreenshot");
+}
+
+void Pebble::removeScreenshot(const QString &filename)
+{
+ qDebug() << "removing screenshot" << filename;
+ m_iface->call("RemoveScreenshot", filename);
+}
+
+void Pebble::performFirmwareUpgrade()
+{
+ m_iface->call("PerformFirmwareUpgrade");
+}
+
+void Pebble::dumpLogs(const QString &filename)
+{
+ m_iface->call("DumpLogs", filename);
+}
diff --git a/rockwork/pebble.h b/rockwork/pebble.h
new file mode 100644
index 0000000..58e13a1
--- /dev/null
+++ b/rockwork/pebble.h
@@ -0,0 +1,131 @@
+#ifndef PEBBLE_H
+#define PEBBLE_H
+
+#include <QObject>
+#include <QDBusInterface>
+
+class NotificationSourceModel;
+class ApplicationsModel;
+class ScreenshotModel;
+
+class Pebble : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString name READ name CONSTANT)
+ Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
+ Q_PROPERTY(QString hardwarePlatform READ hardwarePlatform NOTIFY hardwarePlatformChanged)
+ Q_PROPERTY(int model READ model NOTIFY modelChanged)
+ Q_PROPERTY(NotificationSourceModel* notifications READ notifications CONSTANT)
+ Q_PROPERTY(ApplicationsModel* installedApps READ installedApps CONSTANT)
+ Q_PROPERTY(ApplicationsModel* installedWatchfaces READ installedWatchfaces CONSTANT)
+ Q_PROPERTY(ScreenshotModel* screenshots READ screenshots CONSTANT)
+ Q_PROPERTY(bool recovery READ recovery NOTIFY connectedChanged)
+ Q_PROPERTY(QString softwareVersion READ softwareVersion NOTIFY connectedChanged)
+ Q_PROPERTY(bool firmwareUpgradeAvailable READ firmwareUpgradeAvailable NOTIFY firmwareUpgradeAvailableChanged)
+ Q_PROPERTY(QString firmwareReleaseNotes READ firmwareReleaseNotes NOTIFY firmwareUpgradeAvailableChanged)
+ Q_PROPERTY(QString candidateVersion READ candidateVersion NOTIFY firmwareUpgradeAvailableChanged)
+ Q_PROPERTY(bool upgradingFirmware READ upgradingFirmware NOTIFY upgradingFirmwareChanged)
+ Q_PROPERTY(QVariantMap healthParams READ healthParams WRITE setHealthParams NOTIFY healthParamsChanged)
+ Q_PROPERTY(bool imperialUnits READ imperialUnits WRITE setImperialUnits NOTIFY imperialUnitsChanged)
+ Q_PROPERTY(bool calendarSyncEnabled READ calendarSyncEnabled WRITE setCalendarSyncEnabled NOTIFY calendarSyncEnabledChanged)
+
+public:
+ explicit Pebble(const QDBusObjectPath &path, QObject *parent = 0);
+
+ QDBusObjectPath path();
+
+ bool connected() const;
+ QString address() const;
+ QString name() const;
+ QString hardwarePlatform() const;
+ QString serialNumber() const;
+ QString softwareVersion() const;
+ int model() const;
+ bool recovery() const;
+ bool upgradingFirmware() const;
+
+ NotificationSourceModel *notifications() const;
+ ApplicationsModel* installedApps() const;
+ ApplicationsModel* installedWatchfaces() const;
+ ScreenshotModel* screenshots() const;
+
+ bool firmwareUpgradeAvailable() const;
+ QString firmwareReleaseNotes() const;
+ QString candidateVersion() const;
+
+ QVariantMap healthParams() const;
+ void setHealthParams(const QVariantMap &healthParams);
+
+ bool imperialUnits() const;
+ void setImperialUnits(bool imperialUnits);
+
+ bool calendarSyncEnabled() const;
+ void setCalendarSyncEnabled(bool enabled);
+
+public slots:
+ void setNotificationFilter(const QString &sourceId, bool enabled);
+ void removeApp(const QString &uuid);
+ void installApp(const QString &storeId);
+ void sideloadApp(const QString &packageFile);
+ void moveApp(const QString &uuid, int toIndex);
+ void requestConfigurationURL(const QString &uuid);
+ void configurationClosed(const QString &uuid, const QString &url);
+ void launchApp(const QString &uuid);
+ void requestScreenshot();
+ void removeScreenshot(const QString &filename);
+ void performFirmwareUpgrade();
+ void dumpLogs(const QString &filename);
+
+signals:
+ void connectedChanged();
+ void hardwarePlatformChanged();
+ void modelChanged();
+ void firmwareUpgradeAvailableChanged();
+ void upgradingFirmwareChanged();
+ void logsDumped(bool success);
+ void healthParamsChanged();
+ void imperialUnitsChanged();
+ void calendarSyncEnabledChanged();
+
+ void openURL(const QString &uuid, const QString &url);
+
+private:
+ QVariant fetchProperty(const QString &propertyName) const;
+
+private slots:
+ void dataChanged();
+ void pebbleConnected();
+ void pebbleDisconnected();
+ void notificationFilterChanged(const QString &sourceId, bool enabled);
+ void refreshNotifications();
+ void refreshApps();
+ void appsSorted();
+ void refreshScreenshots();
+ void screenshotAdded(const QString &filename);
+ void screenshotRemoved(const QString &filename);
+ void refreshFirmwareUpdateInfo();
+
+private:
+ QDBusObjectPath m_path;
+
+ bool m_connected = false;
+ QString m_address;
+ QString m_name;
+ QString m_hardwarePlatform;
+ QString m_serialNumber;
+ QString m_softwareVersion;
+ bool m_recovery = false;
+ int m_model = 0;
+ QDBusInterface *m_iface;
+ NotificationSourceModel *m_notifications;
+ ApplicationsModel *m_installedApps;
+ ApplicationsModel *m_installedWatchfaces;
+ ScreenshotModel *m_screenshotModel;
+
+ bool m_firmwareUpgradeAvailable = false;
+ QString m_firmwareReleaseNotes;
+ QString m_candidateVersion;
+ bool m_upgradingFirmware = false;
+};
+
+#endif // PEBBLE_H
diff --git a/rockwork/pebbles.cpp b/rockwork/pebbles.cpp
new file mode 100644
index 0000000..e45691e
--- /dev/null
+++ b/rockwork/pebbles.cpp
@@ -0,0 +1,180 @@
+#include "pebbles.h"
+#include "pebble.h"
+
+#include <QDBusConnection>
+#include <QDBusInterface>
+#include <QDebug>
+#include <QDBusArgument>
+#include <QDBusServiceWatcher>
+#include <algorithm>
+
+#define ROCKWORK_SERVICE QStringLiteral("org.rockwork")
+#define ROCKWORK_MANAGER_PATH QStringLiteral("/org/rockwork/Manager")
+#define ROCKWORK_MANAGER_INTERFACE QStringLiteral("org.rockwork.Manager")
+
+Pebbles::Pebbles(QObject *parent):
+ QAbstractListModel(parent)
+{
+ refresh();
+ m_watcher = new QDBusServiceWatcher(ROCKWORK_SERVICE, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this);
+ QDBusConnection::sessionBus().connect(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE, "PebblesChanged", this, SLOT(refresh()));
+ connect(m_watcher, &QDBusServiceWatcher::serviceRegistered, [this]() {
+ refresh();
+ QDBusConnection::sessionBus().connect(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE, "PebblesChanged", this, SLOT(refresh()));
+ });
+}
+
+int Pebbles::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_pebbles.count();
+}
+
+QVariant Pebbles::data(const QModelIndex &index, int role) const
+{
+ switch (role) {
+ case RoleAddress:
+ return m_pebbles.at(index.row())->address();
+ case RoleName:
+ return m_pebbles.at(index.row())->name();
+ case RoleSerialNumber:
+ return m_pebbles.at(index.row())->serialNumber();
+ case RoleConnected:
+ return m_pebbles.at(index.row())->connected();
+ }
+
+ return QVariant();
+}
+
+QHash<int, QByteArray> Pebbles::roleNames() const
+{
+ QHash<int,QByteArray> roles;
+ roles.insert(RoleAddress, "address");
+ roles.insert(RoleName, "name");
+ roles.insert(RoleSerialNumber, "serialNumber");
+ roles.insert(RoleConnected, "connected");
+ return roles;
+}
+
+QString Pebbles::version() const
+{
+ QDBusInterface iface(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE);
+ if (!iface.isValid()) {
+ qWarning() << "Could not connect to rockworkd.";
+ return QString();
+ }
+ QDBusMessage reply = iface.call("Version");
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "Error refreshing watches:" << reply.errorMessage();
+ return QString();
+ }
+ if (reply.arguments().count() == 0) {
+ qWarning() << "No reply from service.";
+ return QString();
+ }
+ return reply.arguments().first().toString();
+}
+
+Pebble *Pebbles::get(int index) const
+{
+ return m_pebbles.at(index);
+}
+
+int Pebbles::find(const QString &address) const
+{
+ for (int i = 0; i < m_pebbles.count(); i++) {
+ if (m_pebbles.at(i)->address() == address) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+void Pebbles::refresh()
+{
+ qDebug() << "pebbles changed";
+ QDBusInterface iface(ROCKWORK_SERVICE, ROCKWORK_MANAGER_PATH, ROCKWORK_MANAGER_INTERFACE);
+ if (!iface.isValid()) {
+ qWarning() << "Could not connect to rockworkd.";
+ return;
+ }
+ QDBusMessage reply = iface.call("ListWatches");
+ if (reply.type() == QDBusMessage::ErrorMessage) {
+ qWarning() << "Error refreshing watches:" << reply.errorMessage();
+ return;
+ }
+ if (reply.arguments().count() == 0) {
+ qWarning() << "No reply from service.";
+ return;
+ }
+ QDBusArgument arg = reply.arguments().first().value<QDBusArgument>();
+ QStringList availableList;
+ arg.beginArray();
+ while (!arg.atEnd()) {
+ QDBusObjectPath p;
+ arg >> p;
+ if (find(p) == -1) {
+ Pebble *pebble = new Pebble(p, this);
+ connect(pebble, &Pebble::connectedChanged, this, &Pebbles::pebbleConnectedChanged);
+ beginInsertRows(QModelIndex(), m_pebbles.count(), m_pebbles.count());
+ m_pebbles.append(pebble);
+ endInsertRows();
+ emit countChanged();
+ }
+ availableList << p.path();
+ std::sort(m_pebbles.begin(), m_pebbles.end(), Pebbles::sortPebbles);
+ }
+ arg.endArray();
+
+ QList<Pebble*> toRemove;
+ foreach (Pebble *pebble, m_pebbles) {
+ bool found = false;
+ foreach (const QString &path, availableList) {
+ if (path == pebble->path().path()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ toRemove << pebble;
+ }
+ }
+
+ while (!toRemove.isEmpty()) {
+ Pebble *pebble = toRemove.takeFirst();
+ int idx = m_pebbles.indexOf(pebble);
+ beginRemoveRows(QModelIndex(), idx, idx);
+ m_pebbles.takeAt(idx)->deleteLater();
+ endRemoveRows();
+ emit countChanged();
+ }
+}
+
+bool Pebbles::sortPebbles(Pebble *a, Pebble *b)
+{
+ if (a->connected() && !b->connected()) {
+ return true;
+ }
+ else if (!a->connected() && b->connected()) {
+ return false;
+ }
+ else {
+ return a->name() < b->name();
+ }
+}
+
+void Pebbles::pebbleConnectedChanged()
+{
+ Pebble *pebble = static_cast<Pebble*>(sender());
+ emit dataChanged(index(find(pebble->address())), index(find(pebble->address())), {RoleConnected});
+}
+
+int Pebbles::find(const QDBusObjectPath &path) const
+{
+ for (int i = 0; i < m_pebbles.count(); i++) {
+ if (m_pebbles.at(i)->path() == path) {
+ return i;
+ }
+ }
+ return -1;
+}
diff --git a/rockwork/pebbles.h b/rockwork/pebbles.h
new file mode 100644
index 0000000..0fef3bb
--- /dev/null
+++ b/rockwork/pebbles.h
@@ -0,0 +1,56 @@
+#ifndef PEBBLES_H
+#define PEBBLES_H
+
+#include <QObject>
+#include <QAbstractListModel>
+#include <QDBusServiceWatcher>
+#include <QDBusObjectPath>
+
+class Pebble;
+class QDBusInterface;
+
+class Pebbles : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString version READ version)
+
+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
+public:
+ enum Roles {
+ RoleAddress,
+ RoleName,
+ RoleSerialNumber,
+ RoleConnected
+ };
+
+ Pebbles(QObject *parent = 0);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ QString version() const;
+
+ Q_INVOKABLE Pebble *get(int index) const;
+ int find(const QString &address) const;
+
+
+signals:
+ void countChanged();
+
+private slots:
+ void refresh();
+
+ void pebbleConnectedChanged();
+
+private:
+ int find(const QDBusObjectPath &path) const;
+ static bool sortPebbles(Pebble *a, Pebble *b);
+
+private:
+ QDBusInterface *m_iface;
+ QList<Pebble*> m_pebbles;
+ QDBusServiceWatcher *m_watcher;
+};
+
+#endif // PEBBLES_H
diff --git a/rockwork/rockwork.apparmor b/rockwork/rockwork.apparmor
new file mode 100644
index 0000000..9756323
--- /dev/null
+++ b/rockwork/rockwork.apparmor
@@ -0,0 +1,7 @@
+{
+ "policy_groups": [
+ "networking"
+ ],
+ "policy_version": 1.3,
+ "template": "unconfined"
+}
diff --git a/rockwork/rockwork.desktop b/rockwork/rockwork.desktop
new file mode 100644
index 0000000..0a75199
--- /dev/null
+++ b/rockwork/rockwork.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Name=RockWork
+Exec=rockwork
+Icon=rockwork/rockwork.svg
+Terminal=false
+Type=Application
+X-Ubuntu-Touch=true
+
diff --git a/rockwork/rockwork.pro b/rockwork/rockwork.pro
new file mode 100644
index 0000000..43b1dea
--- /dev/null
+++ b/rockwork/rockwork.pro
@@ -0,0 +1,72 @@
+TEMPLATE = app
+TARGET = rockwork
+
+include(../version.pri)
+load(ubuntu-click)
+
+QT += qml quick dbus
+
+CONFIG += c++11
+
+HEADERS += \
+ notificationsourcemodel.h \
+ servicecontrol.h \
+ pebble.h \
+ pebbles.h \
+ applicationsmodel.h \
+ applicationsfiltermodel.h \
+ appstoreclient.h \
+ screenshotmodel.h
+
+SOURCES += main.cpp \
+ notificationsourcemodel.cpp \
+ servicecontrol.cpp \
+ pebble.cpp \
+ pebbles.cpp \
+ applicationsmodel.cpp \
+ applicationsfiltermodel.cpp \
+ appstoreclient.cpp \
+ screenshotmodel.cpp
+
+RESOURCES += rockwork.qrc
+
+QML_FILES += $$files(*.qml,true) \
+ $$files(*.js,true)
+
+CONF_FILES += rockwork.apparmor \
+ rockwork.svg \
+ rockwork.desktop \
+ rockwork.url-dispatcher
+
+AP_TEST_FILES += tests/autopilot/run \
+ $$files(tests/*.py,true)
+
+#show all the files in QtCreator
+OTHER_FILES += $${CONF_FILES} \
+ $${QML_FILES} \
+ $${AP_TEST_FILES} \
+
+
+#specify where the config files are installed to
+config_files.path = /rockwork
+config_files.files += $${CONF_FILES}
+INSTALLS+=config_files
+
+#install the desktop file, a translated version is
+#automatically created in the build directory
+desktop_file.path = /rockwork
+desktop_file.files = $$OUT_PWD/rockwork.desktop
+desktop_file.CONFIG += no_check_exist
+INSTALLS+=desktop_file
+
+# Default rules for deployment.
+target.path = $${UBUNTU_CLICK_BINARY_PATH}
+INSTALLS+=target
+
+DISTFILES += \
+ NotificationsPage.qml \
+ PebblesPage.qml \
+ AppStorePage.qml \
+ AppStoreDetailsPage.qml \
+ PebbleModels.qml \
+ InfoPage.qml
diff --git a/rockwork/rockwork.qrc b/rockwork/rockwork.qrc
new file mode 100644
index 0000000..1d565a1
--- /dev/null
+++ b/rockwork/rockwork.qrc
@@ -0,0 +1,48 @@
+<RCC>
+ <qresource prefix="/">
+ <file>Main.qml</file>
+ <file>NotificationsPage.qml</file>
+ <file>PebblesPage.qml</file>
+ <file>InstalledAppsPage.qml</file>
+ <file>MainMenuPage.qml</file>
+ <file>AppSettingsPage.qml</file>
+ <file>AppStorePage.qml</file>
+ <file>AppStoreDetailsPage.qml</file>
+ <file>InstalledAppDelegate.qml</file>
+ <file>SystemAppIcon.qml</file>
+ <file>ScreenshotsPage.qml</file>
+ <file>snowywhite.svg</file>
+ <file>snowywhite.png</file>
+ <file>artwork/bianca-black.png</file>
+ <file>artwork/bianca-silver.png</file>
+ <file>artwork/black-20mm-hole.png</file>
+ <file>artwork/bobby-black.png</file>
+ <file>artwork/bobby-gold.png</file>
+ <file>artwork/bobby-silver.png</file>
+ <file>artwork/snowy-black.png</file>
+ <file>artwork/snowy-red.png</file>
+ <file>artwork/snowy-white.png</file>
+ <file>artwork/spalding-14mm-black.png</file>
+ <file>artwork/spalding-14mm-rose-gold.png</file>
+ <file>artwork/spalding-14mm-silver.png</file>
+ <file>artwork/spalding-20mm-black.png</file>
+ <file>artwork/spalding-20mm-silver.png</file>
+ <file>artwork/tintin-black.png</file>
+ <file>artwork/tintin-blue.png</file>
+ <file>artwork/tintin-green.png</file>
+ <file>artwork/tintin-grey.png</file>
+ <file>artwork/tintin-orange.png</file>
+ <file>artwork/tintin-pink.png</file>
+ <file>artwork/tintin-red.png</file>
+ <file>artwork/tintin-white.png</file>
+ <file>PebbleModels.qml</file>
+ <file>FirmwareUpgradePage.qml</file>
+ <file>InfoPage.qml</file>
+ <file>artwork/rockwork.svg</file>
+ <file>DeveloperToolsPage.qml</file>
+ <file>ContentPeerPickerPage.qml</file>
+ <file>HealthSettingsDialog.qml</file>
+ <file>SettingsPage.qml</file>
+ <file>ImportPackagePage.qml</file>
+ </qresource>
+</RCC>
diff --git a/rockwork/rockwork.svg b/rockwork/rockwork.svg
new file mode 100644
index 0000000..e4e92c0
--- /dev/null
+++ b/rockwork/rockwork.svg
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="72.248886mm"
+ height="72.248886mm"
+ viewBox="0 0 255.99999 255.99999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="upebble.svg">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ style="color-interpolation-filters:sRGB"
+ id="filter4248"
+ x="-0.025328101"
+ width="1.0506562"
+ y="-0.013960773"
+ height="1.0279215">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="2.3907822"
+ id="feGaussianBlur4250" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="89.121544"
+ inkscape:cy="77.044911"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2880"
+ inkscape:window-height="1752"
+ inkscape:window-x="0"
+ inkscape:window-y="48"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136"
+ originx="-40.000001"
+ originy="-539"
+ snapvisiblegridlinesonly="true"
+ enabled="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-40,-257.36221)">
+ <rect
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4200"
+ width="256"
+ height="256"
+ x="40"
+ y="257.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:0.0479798;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 40,257.36221 256,0 -256,256 z"
+ id="rect4252"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <g
+ id="g4300">
+ <g
+ style="fill:#000000;fill-opacity:1;opacity:0.291;filter:url(#filter4248)"
+ id="g4202"
+ transform="matrix(0.60632857,0,0,0.60632857,-37.462675,74.399202)">
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="path4204"
+ d="m 437.97969,445.08937 c 0,0 11.49464,-4.59544 12.27285,0.25253 0.77821,4.84797 2.06459,45.23266 2.06459,45.23266 -8.36034,0.32794 -13.15013,-0.0886 -13.15013,-0.0886 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path4206"
+ d="m 439.49492,491.6046 12.68287,0.70015 0.54937,42.27954 -13.73731,0.25254 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4208"
+ width="120"
+ height="95"
+ x="280"
+ y="623.36218" />
+ <rect
+ y="307.36221"
+ x="280"
+ height="95"
+ width="120"
+ id="rect4210"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="path4212"
+ d="M 228.0862,442.4309 C 228.58744,435.98794 240,437.36221 240,437.36221 l 0,42.02031 c 0,0 -14.31567,-1.22669 -13.80125,-2.84014 0.51442,-1.61345 1.3862,-27.66851 1.88745,-34.11148 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 420,643.715 337.60905,658.36221 255,643.715 l 0,-20 165,0 z"
+ id="path4214"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4216"
+ d="M 255,382.36221 337.39095,367.715 420,382.36221 l 0,20 -165,0 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="path4218"
+ d="m 438.52906,535.82255 c 0,0 5.15979,0.84007 13.83236,0.44761 0.13423,13.76866 -1.20901,37.74804 -1.85634,42.35471 -0.64733,4.60667 -11.01016,-0.50508 -11.01016,-0.50508 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="path4220"
+ d="m 244.85206,406.56127 c 16.07143,-13.92858 66.12644,-13.34299 97.65304,-13.30725 31.5266,0.0357 73.56632,-0.53467 90.70918,15.17961 17.14286,15.71428 12.91706,70.98675 13.01566,106.0726 0.0986,35.08586 5.19864,81.42741 -13.01565,99.64169 C 415,632.3622 371.12033,628.47664 339.15317,628.18658 307.186,627.89652 263.91063,632.7014 245.3392,615.91569 226.76777,599.12997 231.43107,540.45867 231.61582,505.9352 c 0.18475,-34.52347 -2.83519,-85.44536 13.23624,-99.37393 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ ry="20"
+ rx="20"
+ y="422.36221"
+ x="260"
+ height="174.99998"
+ width="155"
+ id="rect4222"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ y="442.36221"
+ x="280"
+ height="135"
+ width="120"
+ id="rect4224"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="path4226"
+ d="m 375,442.36221 25,0 0,135 -25,0 0,-109.75206 -7.32361,-5.3033 7.32361,-4.9245 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ sodipodi:nodetypes="czccc"
+ inkscape:connector-curvature="0"
+ id="rect4177"
+ d="m 228.09692,344.2696 c 0,0 6.96953,-2.78634 7.44138,0.15312 0.47185,2.93946 1.25182,27.42585 1.25182,27.42585 -5.06911,0.19884 -7.9733,-0.0537 -7.9733,-0.0537 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="rect4179"
+ d="m 229.01565,372.47312 7.68999,0.42452 0.3331,25.63529 -8.32933,0.15312 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4167"
+ width="72.75943"
+ height="57.601215"
+ x="132.30933"
+ y="452.36151" />
+ <rect
+ y="260.76169"
+ x="132.30933"
+ height="57.601215"
+ width="72.75943"
+ id="rect4165"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zcczz"
+ inkscape:connector-curvature="0"
+ id="rect4169"
+ d="m 100.8325,342.6577 c 0.30392,-3.90655 7.22368,-3.07329 7.22368,-3.07329 l 0,25.47811 c 0,0 -8.679998,-0.74378 -8.36809,-1.72206 0.311907,-0.97828 0.84049,-16.77621 1.14441,-20.68276 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 217.19532,464.702 -49.95598,8.88102 -50.08823,-8.88102 0,-12.12657 100.04421,0 z"
+ id="path4175"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="rect4172"
+ d="m 117.15111,306.23633 49.95599,-8.88102 50.08822,8.88102 0,12.12658 -100.04421,0 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cczcc"
+ inkscape:connector-curvature="0"
+ id="rect4181"
+ d="m 228.43002,399.28372 c 0,0 3.12853,0.50936 8.38696,0.2714 0.0814,8.34833 -0.73306,22.88772 -1.12555,25.68087 -0.3925,2.79316 -6.67578,-0.30624 -6.67578,-0.30624 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="zzzzzzzzz"
+ inkscape:connector-curvature="0"
+ id="rect4149"
+ d="m 110.99812,320.90892 c 9.74457,-8.4453 40.09435,-8.09024 59.20983,-8.06857 19.11548,0.0217 44.60536,-0.32419 54.99957,9.20383 10.39421,9.52802 7.83198,43.0413 7.89177,64.31485 0.0598,21.27356 3.15208,49.37176 -7.89176,60.4156 -11.04385,11.04384 -37.64935,8.68791 -57.03195,8.51204 -19.38261,-0.17587 -45.6217,2.73747 -56.88209,-7.44019 -11.26039,-10.17766 -8.4329,-45.75175 -8.32088,-66.68431 0.11202,-20.93257 -1.71905,-51.80796 8.02551,-60.25325 z"
+ style="opacity:1;fill:#cbcbcb;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 133.22587,330.48948 69.72779,0 c 6.71812,0 11.83367,5.41484 12.12657,12.12657 1.19073,27.28478 1.19022,54.56956 0,81.85435 -0.29278,6.71174 -5.40845,12.12657 -12.12657,12.12657 l -69.72779,0 c -6.71812,0 -11.82926,-5.41504 -12.12657,-12.12657 -1.18246,-26.69356 -1.65764,-53.74075 0,-81.85435 0.39543,-6.70647 5.40845,-12.12657 12.12657,-12.12657 z"
+ id="rect4152"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssssss" />
+ <rect
+ y="342.61606"
+ x="131.62029"
+ height="81.854355"
+ width="72.75943"
+ id="rect4154"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ inkscape:connector-curvature="0"
+ id="rect4156"
+ d="m 189.2215,342.61605 15.15821,0 0,81.85436 -15.15821,0 0,-66.54581 -4.44052,-3.21555 4.44052,-2.98586 z"
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ style="fill:#000000"
+ d="m 143.63128,393.17695 c -0.82884,-0.82884 0.32908,-1.46136 2.67524,-1.46136 3.02637,0 3.07586,-0.7116 0.31911,-4.58902 l -2.10477,-2.96041 2.0287,-2.94922 c 1.15063,-1.67272 2.20678,-4.13671 2.44014,-5.69282 0.40696,-2.71383 0.44252,-2.74828 3.27625,-3.17322 1.57564,-0.23629 4.03472,-1.23453 5.46462,-2.21832 1.42988,-0.98379 2.85437,-1.78871 3.16551,-1.78871 0.31114,0 1.68177,0.80847 3.04585,1.7966 1.40341,1.01663 3.70432,1.98019 5.29992,2.21946 2.92933,0.43928 2.99923,0.52347 3.81465,4.59457 0.23714,1.18397 1.22649,3.16119 2.19857,4.39382 0.97208,1.23265 1.76741,2.42825 1.76741,2.6569 0,0.22864 -0.83031,1.62253 -1.84514,3.09753 -2.44659,3.556 -2.38325,4.49115 0.32038,4.72947 1.19102,0.10499 2.22891,0.46766 2.30641,0.80594 0.15778,0.68864 -33.49245,1.21919 -34.17285,0.53879 z m 27.8729,-2.84521 c 0.009,-0.76112 0.83738,-2.4679 1.83992,-3.79283 l 1.82279,-2.40898 -1.58789,-2.08183 c -0.87334,-1.14501 -1.90888,-3.37431 -2.30119,-4.954 -0.68475,-2.75721 -0.82407,-2.89364 -3.48101,-3.40876 -1.52223,-0.29514 -3.68339,-1.22898 -4.80255,-2.0752 l -2.03486,-1.53859 -2.27046,1.53481 c -2.19134,1.48132 -2.85113,1.74296 -6.34208,2.51493 -1.32103,0.29212 -1.68348,0.80695 -2.01126,2.85674 -0.2199,1.37522 -1.11243,3.57721 -1.98338,4.8933 l -1.58355,2.39291 1.54803,2.0343 c 0.85141,1.11887 1.73796,2.79542 1.97011,3.72567 l 0.42209,1.69138 10.38909,0 c 10.35877,0 10.38914,-0.004 10.4062,-1.38385 z m -16.16204,-1.17442 c -3.00082,-0.83158 -3.29439,-2.70542 -0.30753,-1.96294 1.0994,0.27329 2.9676,0.49689 4.15156,0.49689 1.18396,0 3.05216,-0.2236 4.15155,-0.49689 1.34833,-0.33517 1.9989,-0.27586 1.9989,0.18225 0,1.61808 -6.44052,2.76557 -9.99448,1.78069 z m -1.87921,-5.21512 c -0.23497,-0.23497 -0.42721,-1.51123 -0.42721,-2.83614 0,-2.76084 1.40937,-2.98541 1.73071,-0.27578 0.21201,1.78766 -0.62633,3.78908 -1.3035,3.11192 z m 10.02856,-2.68238 c 0,-1.7768 0.25627,-2.46018 0.92256,-2.46018 0.6663,0 0.92257,0.68338 0.92257,2.46018 0,1.7768 -0.25627,2.46018 -0.92257,2.46018 -0.66629,0 -0.92256,-0.68338 -0.92256,-2.46018 z m -22.75668,2.7677 c 0,-0.69366 0.43166,-0.94682 1.38386,-0.81158 0.76111,0.10809 1.38385,0.47331 1.38385,0.81158 0,0.33828 -0.62274,0.70349 -1.38385,0.81159 -0.9522,0.13523 -1.38386,-0.11793 -1.38386,-0.81159 z m 37.8226,0.30318 c -0.46699,-0.7556 1.22568,-1.49279 2.13592,-0.93022 0.35999,0.22248 0.4953,0.66216 0.30068,0.97707 -0.46943,0.75955 -1.95589,0.73096 -2.4366,-0.0469 z m -32.49939,-13.42132 c -1.23387,-1.36341 -1.29125,-1.95046 -0.19065,-1.95046 1.05106,0 3.00139,2.11681 2.45435,2.66385 -0.65422,0.65422 -1.17438,0.49029 -2.2637,-0.71339 z m 27.38286,0.11435 c 0.23773,-1.20797 2.11252,-2.50883 2.72827,-1.89307 0.48678,0.48678 -1.47947,2.90348 -2.36232,2.90348 -0.31065,0 -0.47533,-0.45468 -0.36595,-1.01041 z m -13.38512,-4.7488 c -0.52662,-1.37234 -0.0519,-3.6431 0.82178,-3.93083 0.51752,-0.17043 0.76881,0.45533 0.76881,1.91443 0,2.29458 -0.99824,3.56005 -1.59059,2.0164 z"
+ id="path4285"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ssscsssssssssssssssscsssscsssscsscsssssssssssssssssssssscssccssssssssssss" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="136.46957"
+ y="405.82156"
+ id="text4342"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan4344"
+ x="136.46957"
+ y="405.82156"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10px;font-family:Ubuntu;-inkscape-font-specification:Ubuntu">Tomorrow</tspan></text>
+ </g>
+</svg>
diff --git a/rockwork/rockwork.url-dispatcher b/rockwork/rockwork.url-dispatcher
new file mode 100644
index 0000000..3453482
--- /dev/null
+++ b/rockwork/rockwork.url-dispatcher
@@ -0,0 +1,5 @@
+[
+ {
+ "protocol": "pebblejs"
+ }
+]
diff --git a/rockwork/screenshotmodel.cpp b/rockwork/screenshotmodel.cpp
new file mode 100644
index 0000000..e943aa6
--- /dev/null
+++ b/rockwork/screenshotmodel.cpp
@@ -0,0 +1,71 @@
+#include "screenshotmodel.h"
+
+#include <QDebug>
+
+ScreenshotModel::ScreenshotModel(QObject *parent):
+ QAbstractListModel(parent)
+{
+}
+
+int ScreenshotModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_files.count();
+}
+
+QVariant ScreenshotModel::data(const QModelIndex &index, int role) const
+{
+ switch (role) {
+ case RoleFileName:
+ return m_files.at(index.row());
+ }
+ return QVariant();
+}
+
+QHash<int, QByteArray> ScreenshotModel::roleNames() const
+{
+ QHash<int, QByteArray> roles;
+ roles.insert(RoleFileName, "filename");
+ return roles;
+}
+
+QString ScreenshotModel::get(int index) const
+{
+ if (index >= 0 && index < m_files.count()) {
+ return m_files.at(index);
+ }
+ return QString();
+}
+
+QString ScreenshotModel::latestScreenshot() const
+{
+ return get(0);
+}
+
+void ScreenshotModel::clear()
+{
+ beginResetModel();
+ m_files.clear();
+ endResetModel();
+}
+
+void ScreenshotModel::insert(const QString &filename)
+{
+ qDebug() << "should insert filename" << filename;
+ if (!m_files.contains(filename)) {
+ beginInsertRows(QModelIndex(), 0, 0);
+ m_files.prepend(filename);
+ endInsertRows();
+ emit latestScreenshotChanged();
+ }
+}
+
+void ScreenshotModel::remove(const QString &filename)
+{
+ if (m_files.contains(filename)) {
+ int idx = m_files.indexOf(filename);
+ beginRemoveRows(QModelIndex(), idx, idx);
+ m_files.removeOne(filename);
+ endRemoveRows();
+ }
+}
diff --git a/rockwork/screenshotmodel.h b/rockwork/screenshotmodel.h
new file mode 100644
index 0000000..bc855f1
--- /dev/null
+++ b/rockwork/screenshotmodel.h
@@ -0,0 +1,38 @@
+#ifndef SCREENSHOTMODEL_H
+#define SCREENSHOTMODEL_H
+
+#include <QAbstractListModel>
+
+class ScreenshotModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QString latestScreenshot READ latestScreenshot NOTIFY latestScreenshotChanged)
+
+public:
+ enum Role {
+ RoleFileName
+ };
+
+ ScreenshotModel(QObject *parent = nullptr);
+ QString path() const;
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+ Q_INVOKABLE QString get(int index) const;
+ QString latestScreenshot() const;
+
+ void clear();
+ void insert(const QString &filename);
+ void remove(const QString &filename);
+
+signals:
+ void latestScreenshotChanged();
+
+private:
+ QStringList m_files;
+};
+
+#endif // SCREENSHOTMODEL_H
diff --git a/rockwork/servicecontrol.cpp b/rockwork/servicecontrol.cpp
new file mode 100644
index 0000000..4d6903f
--- /dev/null
+++ b/rockwork/servicecontrol.cpp
@@ -0,0 +1,118 @@
+#include "servicecontrol.h"
+
+#include <QFile>
+#include <QDir>
+#include <QDebug>
+#include <QCoreApplication>
+#include <QProcess>
+
+ServiceControl::ServiceControl(QObject *parent) : QObject(parent)
+{
+
+}
+
+QString ServiceControl::serviceName() const
+{
+ return m_serviceName;
+}
+
+void ServiceControl::setServiceName(const QString &serviceName)
+{
+ if (m_serviceName != serviceName) {
+ m_serviceName = serviceName;
+ emit serviceNameChanged();
+ }
+}
+
+bool ServiceControl::serviceFileInstalled() const
+{
+ if (m_serviceName.isEmpty()) {
+ qDebug() << "Service name not set.";
+ return false;
+ }
+ QFile f(QDir::homePath() + "/.config/upstart/" + m_serviceName + ".conf");
+ return f.exists();
+}
+
+bool ServiceControl::installServiceFile()
+{
+ if (m_serviceName.isEmpty()) {
+ qDebug() << "Service name not set. Cannot generate service file.";
+ return false;
+ }
+
+ QFile f(QDir::homePath() + "/.config/upstart/" + m_serviceName + ".conf");
+ if (f.exists()) {
+ qDebug() << "Service file already existing...";
+ return false;
+ }
+
+ if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
+ qDebug() << "Cannot create service file";
+ return false;
+ }
+
+ QString appDir = qApp->applicationDirPath();
+ // Try to replace version with "current" to be more robust against updates
+ appDir.replace(QRegExp("rockwork.mzanetti\/[0-9.]*\/"), "rockwork.mzanetti/current/");
+
+ f.write("start on started unity8\n");
+ f.write("pre-start script\n");
+ f.write(" initctl set-env LD_LIBRARY_PATH=" + appDir.toUtf8() + "/../:$LD_LIBRARY_PATH\n");
+ f.write("end script\n");
+ f.write("exec " + appDir.toUtf8() + "/" + m_serviceName.toUtf8() + "\n");
+ f.close();
+ return true;
+}
+
+bool ServiceControl::removeServiceFile()
+{
+ if (m_serviceName.isEmpty()) {
+ qDebug() << "Service name not set.";
+ return false;
+ }
+ QFile f(QDir::homePath() + "/.config/upstart/" + m_serviceName + ".conf");
+ return f.remove();
+}
+
+bool ServiceControl::serviceRunning() const
+{
+ QProcess p;
+ p.start("initctl", {"status", m_serviceName});
+ p.waitForFinished();
+ QByteArray output = p.readAll();
+ qDebug() << output;
+ return output.contains("running");
+}
+
+bool ServiceControl::setServiceRunning(bool running)
+{
+ if (running && !serviceRunning()) {
+ return startService();
+ } else if (!running && serviceRunning()) {
+ return stopService();
+ }
+ return true; // Requested state is already the current state.
+}
+
+bool ServiceControl::startService()
+{
+ qDebug() << "should start service";
+ int ret = QProcess::execute("start", {m_serviceName});
+ return ret == 0;
+}
+
+bool ServiceControl::stopService()
+{
+ qDebug() << "should stop service";
+ int ret = QProcess::execute("stop", {m_serviceName});
+ return ret == 0;
+}
+
+bool ServiceControl::restartService()
+{
+ qDebug() << "should stop service";
+ int ret = QProcess::execute("restart", {m_serviceName});
+ return ret == 0;
+}
+
diff --git a/rockwork/servicecontrol.h b/rockwork/servicecontrol.h
new file mode 100644
index 0000000..4689506
--- /dev/null
+++ b/rockwork/servicecontrol.h
@@ -0,0 +1,38 @@
+#ifndef SERVICECONTROL_H
+#define SERVICECONTROL_H
+
+#include <QObject>
+
+class ServiceControl : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString serviceName READ serviceName WRITE setServiceName NOTIFY serviceNameChanged)
+ Q_PROPERTY(bool serviceFileInstalled READ serviceFileInstalled NOTIFY serviceFileInstalledChanged)
+ Q_PROPERTY(bool serviceRunning READ serviceRunning WRITE setServiceRunning NOTIFY serviceRunningChanged)
+
+public:
+ explicit ServiceControl(QObject *parent = 0);
+
+ QString serviceName() const;
+ void setServiceName(const QString &serviceName);
+
+ bool serviceFileInstalled() const;
+ Q_INVOKABLE bool installServiceFile();
+ Q_INVOKABLE bool removeServiceFile();
+
+ bool serviceRunning() const;
+ bool setServiceRunning(bool running);
+ Q_INVOKABLE bool startService();
+ Q_INVOKABLE bool stopService();
+ Q_INVOKABLE bool restartService();
+
+signals:
+ void serviceNameChanged();
+ void serviceFileInstalledChanged();
+ void serviceRunningChanged();
+
+private:
+ QString m_serviceName;
+};
+
+#endif // SERVICECONTROL_H
diff --git a/rockwork/snowywhite.png b/rockwork/snowywhite.png
new file mode 100644
index 0000000..1a354b4
--- /dev/null
+++ b/rockwork/snowywhite.png
Binary files differ
diff --git a/rockwork/snowywhite.svg b/rockwork/snowywhite.svg
new file mode 100644
index 0000000..0544670
--- /dev/null
+++ b/rockwork/snowywhite.svg
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="72.248886mm"
+ height="72.248886mm"
+ viewBox="0 0 255.99999 255.99999"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="snowywhite.svg">
+ <defs
+ id="defs4">
+ <filter
+ inkscape:collect="always"
+ style="color-interpolation-filters:sRGB"
+ id="filter4364"
+ x="-0.059098901"
+ width="1.1181978"
+ y="-0.032575137"
+ height="1.0651503">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="5.5784918"
+ id="feGaussianBlur4366" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="89.121544"
+ inkscape:cy="77.044911"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2880"
+ inkscape:window-height="1752"
+ inkscape:window-x="0"
+ inkscape:window-y="48"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4136"
+ originx="-40.000001"
+ originy="-539"
+ snapvisiblegridlinesonly="true"
+ enabled="false" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-40,-257.36221)">
+ <g
+ transform="matrix(0.60632857,0,0,0.60632857,-37.462675,74.399202)"
+ id="g4202"
+ style="opacity:0.581;fill:#000000;fill-opacity:1;filter:url(#filter4364)">
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 437.97969,445.08937 c 0,0 11.49464,-4.59544 12.27285,0.25253 0.77821,4.84797 2.06459,45.23266 2.06459,45.23266 -8.36034,0.32794 -13.15013,-0.0886 -13.15013,-0.0886 z"
+ id="path4204"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="czccc" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 439.49492,491.6046 12.68287,0.70015 0.54937,42.27954 -13.73731,0.25254 z"
+ id="path4206"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <rect
+ y="623.36218"
+ x="280"
+ height="95"
+ width="120"
+ id="rect4208"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4210"
+ width="120"
+ height="95"
+ x="280"
+ y="307.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 228.0862,442.4309 C 228.58744,435.98794 240,437.36221 240,437.36221 l 0,42.02031 c 0,0 -14.31567,-1.22669 -13.80125,-2.84014 0.51442,-1.61345 1.3862,-27.66851 1.88745,-34.11148 z"
+ id="path4212"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zcczz" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4214"
+ d="M 420,643.715 337.60905,658.36221 255,643.715 l 0,-20 165,0 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 255,382.36221 337.39095,367.715 420,382.36221 l 0,20 -165,0 z"
+ id="path4216"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 438.52906,535.82255 c 0,0 5.15979,0.84007 13.83236,0.44761 0.13423,13.76866 -1.20901,37.74804 -1.85634,42.35471 -0.64733,4.60667 -11.01016,-0.50508 -11.01016,-0.50508 z"
+ id="path4218"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cczcc" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 244.85206,406.56127 c 16.07143,-13.92858 66.12644,-13.34299 97.65304,-13.30725 31.5266,0.0357 73.56632,-0.53467 90.70918,15.17961 17.14286,15.71428 12.91706,70.98675 13.01566,106.0726 0.0986,35.08586 5.19864,81.42741 -13.01565,99.64169 C 415,632.3622 371.12033,628.47664 339.15317,628.18658 307.186,627.89652 263.91063,632.7014 245.3392,615.91569 226.76777,599.12997 231.43107,540.45867 231.61582,505.9352 c 0.18475,-34.52347 -2.83519,-85.44536 13.23624,-99.37393 z"
+ id="path4220"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zzzzzzzzz" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4222"
+ width="155"
+ height="174.99998"
+ x="260"
+ y="422.36221"
+ rx="20"
+ ry="20" />
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4224"
+ width="120"
+ height="135"
+ x="280"
+ y="442.36221" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 375,442.36221 25,0 0,135 -25,0 0,-109.75206 -7.32361,-5.3033 7.32361,-4.9245 z"
+ id="path4226"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 228.09692,344.2696 c 0,0 6.96953,-2.78634 7.44138,0.15312 0.47185,2.93946 1.25182,27.42585 1.25182,27.42585 -5.06911,0.19884 -7.9733,-0.0537 -7.9733,-0.0537 z"
+ id="rect4177"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="czccc" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 229.01565,372.47312 7.68999,0.42452 0.3331,25.63529 -8.32933,0.15312 z"
+ id="rect4179"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <rect
+ y="452.36151"
+ x="132.30933"
+ height="57.601215"
+ width="72.75943"
+ id="rect4167"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4165"
+ width="72.75943"
+ height="57.601215"
+ x="132.30933"
+ y="260.76169" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 100.8325,342.6577 c 0.30392,-3.90655 7.22368,-3.07329 7.22368,-3.07329 l 0,25.47811 c 0,0 -8.679998,-0.74378 -8.36809,-1.72206 0.311907,-0.97828 0.84049,-16.77621 1.14441,-20.68276 z"
+ id="rect4169"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zcczz" />
+ <path
+ sodipodi:nodetypes="cccccc"
+ inkscape:connector-curvature="0"
+ id="path4175"
+ d="m 217.19532,464.702 -49.95598,8.88102 -50.08823,-8.88102 0,-12.12657 100.04421,0 z"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 117.15111,306.23633 49.95599,-8.88102 50.08822,8.88102 0,12.12658 -100.04421,0 z"
+ id="rect4172"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 228.43002,399.28372 c 0,0 3.12853,0.50936 8.38696,0.2714 0.0814,8.34833 -0.73306,22.88772 -1.12555,25.68087 -0.3925,2.79316 -6.67578,-0.30624 -6.67578,-0.30624 z"
+ id="rect4181"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cczcc" />
+ <path
+ style="opacity:1;fill:#cbcbcb;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 110.99812,320.90892 c 9.74457,-8.4453 40.09435,-8.09024 59.20983,-8.06857 19.11548,0.0217 44.60536,-0.32419 54.99957,9.20383 10.39421,9.52802 7.83198,43.0413 7.89177,64.31485 0.0598,21.27356 3.15208,49.37176 -7.89176,60.4156 -11.04385,11.04384 -37.64935,8.68791 -57.03195,8.51204 -19.38261,-0.17587 -45.6217,2.73747 -56.88209,-7.44019 -11.26039,-10.17766 -8.4329,-45.75175 -8.32088,-66.68431 0.11202,-20.93257 -1.71905,-51.80796 8.02551,-60.25325 z"
+ id="rect4149"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="zzzzzzzzz" />
+ <path
+ sodipodi:nodetypes="sssssssss"
+ inkscape:connector-curvature="0"
+ id="rect4152"
+ d="m 133.22587,330.48948 69.72779,0 c 6.71812,0 11.83367,5.41484 12.12657,12.12657 1.19073,27.28478 1.19022,54.56956 0,81.85435 -0.29278,6.71174 -5.40845,12.12657 -12.12657,12.12657 l -69.72779,0 c -6.71812,0 -11.82926,-5.41504 -12.12657,-12.12657 -1.18246,-26.69356 -1.65764,-53.74075 0,-81.85435 0.39543,-6.70647 5.40845,-12.12657 12.12657,-12.12657 z"
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect4154"
+ width="72.75943"
+ height="81.854355"
+ x="131.62029"
+ y="342.61606" />
+ <path
+ style="opacity:1;fill:#78d3fc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 189.2215,342.61605 15.15821,0 0,81.85436 -15.15821,0 0,-66.54581 -4.44052,-3.21555 4.44052,-2.98586 z"
+ id="rect4156"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+</svg>
diff --git a/rockworkd/core.cpp b/rockworkd/core.cpp
new file mode 100644
index 0000000..38a25c5
--- /dev/null
+++ b/rockworkd/core.cpp
@@ -0,0 +1,51 @@
+#include "core.h"
+
+#include "pebblemanager.h"
+#include "dbusinterface.h"
+
+#include "platformintegration/ubuntu/ubuntuplatform.h"
+#ifdef ENABLE_TESTING
+#include "platformintegration/testing/testingplatform.h"
+#endif
+
+#include <QDebug>
+
+Core* Core::s_instance = nullptr;
+
+Core *Core::instance()
+{
+ if (!s_instance) {
+ s_instance = new Core();
+ }
+ return s_instance;
+}
+
+PebbleManager *Core::pebbleManager()
+{
+ return m_pebbleManager;
+}
+
+PlatformInterface *Core::platform()
+{
+ return m_platform;
+}
+
+Core::Core(QObject *parent):
+ QObject(parent)
+{
+}
+
+void Core::init()
+{
+ // Platform integration
+#ifdef ENABLE_TESTING
+ m_platform = new TestingPlatform(this);
+#else
+ m_platform = new UbuntuPlatform(this);
+#endif
+
+ m_pebbleManager = new PebbleManager(this);
+
+ m_dbusInterface = new DBusInterface(this);
+}
+
diff --git a/rockworkd/core.h b/rockworkd/core.h
new file mode 100644
index 0000000..4791aac
--- /dev/null
+++ b/rockworkd/core.h
@@ -0,0 +1,33 @@
+#ifndef CORE_H
+#define CORE_H
+
+#include <QObject>
+#include <QTimer>
+
+class PebbleManager;
+class DBusInterface;
+class PlatformInterface;
+
+class Core : public QObject
+{
+ Q_OBJECT
+public:
+ static Core *instance();
+
+ PebbleManager* pebbleManager();
+ PlatformInterface* platform();
+
+ void init();
+private:
+ explicit Core(QObject *parent = 0);
+ static Core *s_instance;
+
+private slots:
+
+private:
+ PebbleManager *m_pebbleManager;
+ DBusInterface *m_dbusInterface;
+ PlatformInterface *m_platform;
+};
+
+#endif // CORE_H
diff --git a/rockworkd/dbusinterface.cpp b/rockworkd/dbusinterface.cpp
new file mode 100644
index 0000000..766de62
--- /dev/null
+++ b/rockworkd/dbusinterface.cpp
@@ -0,0 +1,308 @@
+#include "dbusinterface.h"
+#include "core.h"
+#include "pebblemanager.h"
+
+DBusPebble::DBusPebble(Pebble *pebble, QObject *parent):
+ QObject(parent),
+ m_pebble(pebble)
+{
+ connect(pebble, &Pebble::pebbleConnected, this, &DBusPebble::Connected);
+ connect(pebble, &Pebble::pebbleDisconnected, this, &DBusPebble::Disconnected);
+ connect(pebble, &Pebble::installedAppsChanged, this, &DBusPebble::InstalledAppsChanged);
+ connect(pebble, &Pebble::openURL, this, &DBusPebble::OpenURL);
+ connect(pebble, &Pebble::notificationFilterChanged, this, &DBusPebble::NotificationFilterChanged);
+ connect(pebble, &Pebble::screenshotAdded, this, &DBusPebble::ScreenshotAdded);
+ connect(pebble, &Pebble::screenshotRemoved, this, &DBusPebble::ScreenshotRemoved);
+ connect(pebble, &Pebble::updateAvailableChanged, this, &DBusPebble::FirmwareUpgradeAvailableChanged);
+ connect(pebble, &Pebble::upgradingFirmwareChanged, this, &DBusPebble::UpgradingFirmwareChanged);
+ connect(pebble, &Pebble::logsDumped, this, &DBusPebble::LogsDumped);
+ connect(pebble, &Pebble::healtParamsChanged, this, &DBusPebble::HealthParamsChanged);
+ connect(pebble, &Pebble::imperialUnitsChanged, this, &DBusPebble::ImperialUnitsChanged);
+ connect(pebble, &Pebble::calendarSyncEnabledChanged, this, &DBusPebble::CalendarSyncEnabledChanged);
+}
+
+QString DBusPebble::Address() const
+{
+ return m_pebble->address().toString();
+}
+
+QString DBusPebble::Name() const
+{
+ return m_pebble->name();
+}
+
+bool DBusPebble::IsConnected() const
+{
+ return m_pebble->connected();
+}
+
+bool DBusPebble::Recovery() const
+{
+ return m_pebble->recovery();
+}
+
+bool DBusPebble::FirmwareUpgradeAvailable() const
+{
+ return m_pebble->firmwareUpdateAvailable();
+}
+
+QString DBusPebble::FirmwareReleaseNotes() const
+{
+ return m_pebble->firmwareReleaseNotes();
+}
+
+QString DBusPebble::CandidateFirmwareVersion() const
+{
+ return m_pebble->candidateFirmwareVersion();
+}
+
+QVariantMap DBusPebble::NotificationsFilter() const
+{
+ QVariantMap ret;
+ QHash<QString, bool> filter = m_pebble->notificationsFilter();
+ foreach (const QString &sourceId, filter.keys()) {
+ ret.insert(sourceId, filter.value(sourceId));
+ }
+ return ret;
+}
+
+void DBusPebble::SetNotificationFilter(const QString &sourceId, bool enabled)
+{
+ m_pebble->setNotificationFilter(sourceId, enabled);
+}
+
+void DBusPebble::InstallApp(const QString &id)
+{
+ qDebug() << "installapp called" << id;
+ m_pebble->installApp(id);
+}
+
+void DBusPebble::SideloadApp(const QString &packageFile)
+{
+ m_pebble->sideloadApp(packageFile);
+}
+
+QStringList DBusPebble::InstalledAppIds() const
+{
+ QStringList ret;
+ foreach (const QUuid &id, m_pebble->installedAppIds()) {
+ ret << id.toString();
+ }
+ return ret;
+}
+
+QVariantList DBusPebble::InstalledApps() const
+{
+ QVariantList list;
+ foreach (const QUuid &appId, m_pebble->installedAppIds()) {
+ QVariantMap app;
+ AppInfo info = m_pebble->appInfo(appId);
+ app.insert("storeId", info.storeId());
+ app.insert("name", info.shortName());
+ app.insert("vendor", info.companyName());
+ app.insert("watchface", info.isWatchface());
+ app.insert("version", info.versionLabel());
+ app.insert("uuid", info.uuid().toString());
+ app.insert("hasSettings", info.hasSettings());
+ app.insert("icon", info.path() + "/list_image.png");
+ app.insert("systemApp", info.isSystemApp());
+
+ list.append(app);
+ }
+ return list;
+}
+
+void DBusPebble::RemoveApp(const QString &id)
+{
+ m_pebble->removeApp(id);
+}
+
+void DBusPebble::ConfigurationURL(const QString &uuid)
+{
+ m_pebble->requestConfigurationURL(QUuid(uuid));
+}
+
+void DBusPebble::ConfigurationClosed(const QString &uuid, const QString &result)
+{
+ m_pebble->configurationClosed(QUuid(uuid), result);
+}
+
+void DBusPebble::SetAppOrder(const QStringList &newList)
+{
+ QList<QUuid> uuidList;
+ foreach (const QString &id, newList) {
+ uuidList << QUuid(id);
+ }
+ m_pebble->setAppOrder(uuidList);
+}
+
+void DBusPebble::LaunchApp(const QString &uuid)
+{
+ m_pebble->launchApp(QUuid(uuid));
+}
+
+void DBusPebble::RequestScreenshot()
+{
+ m_pebble->requestScreenshot();
+}
+
+QStringList DBusPebble::Screenshots() const
+{
+ return m_pebble->screenshots();
+}
+
+void DBusPebble::RemoveScreenshot(const QString &filename)
+{
+ qDebug() << "Should remove screenshot" << filename;
+ m_pebble->removeScreenshot(filename);
+}
+
+void DBusPebble::PerformFirmwareUpgrade()
+{
+ m_pebble->upgradeFirmware();
+}
+
+bool DBusPebble::UpgradingFirmware() const
+{
+ return m_pebble->upgradingFirmware();
+}
+
+QString DBusPebble::SerialNumber() const
+{
+ return m_pebble->serialNumber();
+}
+
+QString DBusPebble::HardwarePlatform() const
+{
+ switch (m_pebble->hardwarePlatform()) {
+ case HardwarePlatformAplite:
+ return "aplite";
+ case HardwarePlatformBasalt:
+ return "basalt";
+ case HardwarePlatformChalk:
+ return "chalk";
+ default:
+ ;
+ }
+ return "unknown";
+}
+
+QString DBusPebble::SoftwareVersion() const
+{
+ return m_pebble->softwareVersion();
+}
+
+int DBusPebble::Model() const
+{
+ return m_pebble->model();
+}
+
+void DBusPebble::DumpLogs(const QString &fileName) const
+{
+ qDebug() << "dumplogs" << fileName;
+ m_pebble->dumpLogs(fileName);
+}
+
+QVariantMap DBusPebble::HealthParams() const
+{
+ QVariantMap map;
+ map.insert("enabled", m_pebble->healthParams().enabled());
+ map.insert("age", m_pebble->healthParams().age());
+ map.insert("gender", m_pebble->healthParams().gender() == HealthParams::GenderFemale ? "female" : "male");
+ map.insert("height", m_pebble->healthParams().height());
+ map.insert("moreActive", m_pebble->healthParams().moreActive());
+ map.insert("sleepMore", m_pebble->healthParams().sleepMore());
+ map.insert("weight", m_pebble->healthParams().weight());
+ return map;
+}
+
+void DBusPebble::SetHealthParams(const QVariantMap &healthParams)
+{
+ ::HealthParams params;
+ params.setEnabled(healthParams.value("enabled").toBool());
+ params.setAge(healthParams.value("age").toInt());
+ params.setGender(healthParams.value("gender").toString() == "female" ? HealthParams::GenderFemale : HealthParams::GenderMale);
+ params.setHeight(healthParams.value("height").toInt());
+ params.setWeight(healthParams.value("weight").toInt());
+ params.setMoreActive(healthParams.value("moreActive").toBool());
+ params.setSleepMore(healthParams.value("sleepMore").toBool());
+ m_pebble->setHealthParams(params);
+}
+
+bool DBusPebble::ImperialUnits() const
+{
+ return m_pebble->imperialUnits();
+}
+
+void DBusPebble::SetImperialUnits(bool imperialUnits)
+{
+ qDebug() << "setting imperial units" << imperialUnits;
+ m_pebble->setImperialUnits(imperialUnits);
+}
+
+bool DBusPebble::CalendarSyncEnabled() const
+{
+ return m_pebble->calendarSyncEnabled();
+}
+
+void DBusPebble::SetCalendarSyncEnabled(bool enabled)
+{
+ m_pebble->setCalendarSyncEnabled(enabled);
+}
+
+
+DBusInterface::DBusInterface(QObject *parent) :
+ QObject(parent)
+{
+ QDBusConnection::sessionBus().registerService("org.rockwork");
+ QDBusConnection::sessionBus().registerObject("/org/rockwork/Manager", this, QDBusConnection::ExportScriptableSlots|QDBusConnection::ExportScriptableSignals);
+
+ qDebug() << "pebble manager has:" << Core::instance()->pebbleManager()->pebbles().count() << Core::instance()->pebbleManager();
+ foreach (Pebble *pebble, Core::instance()->pebbleManager()->pebbles()) {
+ pebbleAdded(pebble);
+ }
+
+ qDebug() << "connecting dbus iface";
+ connect(Core::instance()->pebbleManager(), &PebbleManager::pebbleAdded, this, &DBusInterface::pebbleAdded);
+ connect(Core::instance()->pebbleManager(), &PebbleManager::pebbleRemoved, this, &DBusInterface::pebbleRemoved);
+}
+
+QString DBusInterface::Version()
+{
+ return QStringLiteral(VERSION);
+}
+
+QList<QDBusObjectPath> DBusInterface::ListWatches()
+{
+ QList<QDBusObjectPath> ret;
+ foreach (const QString &address, m_dbusPebbles.keys()) {
+ ret.append(QDBusObjectPath("/org/rockwork/" + address));
+ }
+ return ret;
+}
+
+void DBusInterface::pebbleAdded(Pebble *pebble)
+{
+ qDebug() << "pebble added";
+ QString address = pebble->address().toString().replace(":", "_");
+ if (m_dbusPebbles.contains(address)) {
+ return;
+ }
+
+ qDebug() << "registering dbus iface";
+ DBusPebble *dbusPebble = new DBusPebble(pebble, this);
+ m_dbusPebbles.insert(address, dbusPebble);
+ QDBusConnection::sessionBus().registerObject("/org/rockwork/" + address, dbusPebble, QDBusConnection::ExportAllContents);
+
+ emit PebblesChanged();
+}
+
+void DBusInterface::pebbleRemoved(Pebble *pebble)
+{
+ QString address = pebble->address().toString().replace(":", "_");
+
+ QDBusConnection::sessionBus().unregisterObject("/org/rockwork/" + address);
+ m_dbusPebbles.remove(address);
+
+ emit PebblesChanged();
+}
diff --git a/rockworkd/dbusinterface.h b/rockworkd/dbusinterface.h
new file mode 100644
index 0000000..0c340f2
--- /dev/null
+++ b/rockworkd/dbusinterface.h
@@ -0,0 +1,102 @@
+#ifndef DBUSINTERFACE_H
+#define DBUSINTERFACE_H
+
+#include <QObject>
+#include <QDBusAbstractAdaptor>
+#include <QDBusObjectPath>
+
+class Pebble;
+
+class DBusPebble: public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.rockwork.Pebble")
+public:
+ DBusPebble(Pebble *pebble, QObject *parent);
+
+signals:
+ void Connected();
+ void Disconnected();
+ void NotificationFilterChanged(const QString &sourceId, bool enabled);
+ void InstalledAppsChanged();
+ void OpenURL(const QString &uuid, const QString &url);
+ void ScreenshotAdded(const QString &filename);
+ void ScreenshotRemoved(const QString &filename);
+ void FirmwareUpgradeAvailableChanged();
+ void UpgradingFirmwareChanged();
+ void LogsDumped(bool success);
+
+ void HealthParamsChanged();
+ void ImperialUnitsChanged();
+ void CalendarSyncEnabledChanged();
+
+public slots:
+ QString Address() const;
+ QString Name() const;
+ QString SerialNumber() const;
+ QString HardwarePlatform() const;
+ QString SoftwareVersion() const;
+ int Model() const;
+ bool IsConnected() const;
+ bool Recovery() const;
+ bool FirmwareUpgradeAvailable() const;
+ QString CandidateFirmwareVersion() const;
+ QString FirmwareReleaseNotes() const;
+ void PerformFirmwareUpgrade();
+ bool UpgradingFirmware() const;
+
+ QVariantMap NotificationsFilter() const;
+ void SetNotificationFilter(const QString &sourceId, bool enabled);
+
+ void InstallApp(const QString &id);
+ void SideloadApp(const QString &packageFile);
+ QStringList InstalledAppIds() const;
+ QVariantList InstalledApps() const;
+ void RemoveApp(const QString &id);
+ void ConfigurationURL(const QString &uuid);
+ void ConfigurationClosed(const QString &uuid, const QString &result);
+ void SetAppOrder(const QStringList &newList);
+ void LaunchApp(const QString &uuid);
+ void RequestScreenshot();
+ QStringList Screenshots() const;
+ void RemoveScreenshot(const QString &filename);
+ void DumpLogs(const QString &fileName) const;
+
+ QVariantMap HealthParams() const;
+ void SetHealthParams(const QVariantMap &healthParams);
+
+ bool ImperialUnits() const;
+ void SetImperialUnits(bool imperialUnits);
+
+ bool CalendarSyncEnabled() const;
+ void SetCalendarSyncEnabled(bool enabled);
+
+private:
+ Pebble *m_pebble;
+};
+
+class DBusInterface : public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.rockwork.Manager")
+
+public:
+ explicit DBusInterface(QObject *parent = 0);
+
+public slots:
+ Q_SCRIPTABLE QString Version();
+ Q_SCRIPTABLE QList<QDBusObjectPath> ListWatches();
+
+signals:
+ Q_SCRIPTABLE void PebblesChanged();
+ void NameChanged();
+
+private slots:
+ void pebbleAdded(Pebble *pebble);
+ void pebbleRemoved(Pebble *pebble);
+
+private:
+ QHash<QString, DBusPebble*> m_dbusPebbles;
+};
+
+#endif // DBUSINTERFACE_H
diff --git a/rockworkd/jsfiles.qrc b/rockworkd/jsfiles.qrc
new file mode 100644
index 0000000..807350d
--- /dev/null
+++ b/rockworkd/jsfiles.qrc
@@ -0,0 +1,2 @@
+<RCC/>
+
diff --git a/rockworkd/libpebble/appdownloader.cpp b/rockworkd/libpebble/appdownloader.cpp
new file mode 100644
index 0000000..acecf0f
--- /dev/null
+++ b/rockworkd/libpebble/appdownloader.cpp
@@ -0,0 +1,113 @@
+#include "appdownloader.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "ziphelper.h"
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QDir>
+#include <QFile>
+#include <QJsonDocument>
+
+AppDownloader::AppDownloader(const QString &storagePath, QObject *parent) :
+ QObject(parent),
+ m_storagePath(storagePath + "/apps/")
+{
+ m_nam = new QNetworkAccessManager(this);
+}
+
+void AppDownloader::downloadApp(const QString &id)
+{
+ QNetworkRequest request(QUrl("https://api2.getpebble.com/v2/apps/id/" + id));
+ QNetworkReply *reply = m_nam->get(request);
+ reply->setProperty("storeId", id);
+ connect(reply, &QNetworkReply::finished, this, &AppDownloader::appJsonFetched);
+}
+
+void AppDownloader::appJsonFetched()
+{
+ QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
+ reply->deleteLater();
+
+ QString storeId = reply->property("storeId").toString();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qWarning() << "Error fetching App Json" << reply->errorString();
+ return;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing App Json" << error.errorString();
+ return;
+ }
+
+ QVariantMap map = jsonDoc.toVariant().toMap();
+ if (!map.contains("data") || map.value("data").toList().length() == 0) {
+ qWarning() << "Unexpected json content:" << jsonDoc.toJson();
+ return;
+ }
+ QVariantMap appMap = map.value("data").toList().first().toMap();
+ QString pbwFileUrl = appMap.value("latest_release").toMap().value("pbw_file").toString();
+ if (pbwFileUrl.isEmpty()) {
+ qWarning() << "pbw file url empty." << jsonDoc.toJson();
+ return;
+ }
+
+ QDir dir;
+ dir.mkpath(m_storagePath + storeId);
+
+ QString iconFile = appMap.value("list_image").toMap().value("144x144").toString();
+ QNetworkRequest request(iconFile);
+ QNetworkReply *imageReply = m_nam->get(request);
+ qDebug() << "fetching image" << iconFile;
+ connect(imageReply, &QNetworkReply::finished, [this, imageReply, storeId]() {
+ imageReply->deleteLater();
+ QString targetFile = m_storagePath + storeId + "/list_image.png";
+ qDebug() << "saving image to" << targetFile;
+ QFile f(targetFile);
+ if (f.open(QFile::WriteOnly)) {
+ f.write(imageReply->readAll());
+ f.close();
+ }
+ });
+
+ fetchPackage(pbwFileUrl, storeId);
+}
+
+void AppDownloader::fetchPackage(const QString &url, const QString &storeId)
+{
+ QNetworkRequest request(url);
+ QNetworkReply *reply = m_nam->get(request);
+ reply->setProperty("storeId", storeId);
+ connect(reply, &QNetworkReply::finished, this, &AppDownloader::packageFetched);
+}
+
+void AppDownloader::packageFetched()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ reply->deleteLater();
+
+ QString storeId = reply->property("storeId").toString();
+
+ QFile f(m_storagePath + storeId + "/" + reply->request().url().fileName() + ".zip");
+ if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Error opening file for writing";
+ return;
+ }
+ f.write(reply->readAll());
+ f.flush();
+ f.close();
+
+ QString zipName = m_storagePath + storeId + "/" + reply->request().url().fileName() + ".zip";
+
+ if (!ZipHelper::unpackArchive(zipName, m_storagePath + storeId)) {
+ qWarning() << "Error unpacking App zip file";
+ return;
+ }
+
+ emit downloadFinished(storeId);
+}
diff --git a/rockworkd/libpebble/appdownloader.h b/rockworkd/libpebble/appdownloader.h
new file mode 100644
index 0000000..6c81c4a
--- /dev/null
+++ b/rockworkd/libpebble/appdownloader.h
@@ -0,0 +1,32 @@
+#ifndef APPDOWNLOADER_H
+#define APPDOWNLOADER_H
+
+#include <QObject>
+#include <QMap>
+
+class QNetworkAccessManager;
+
+class AppDownloader : public QObject
+{
+ Q_OBJECT
+public:
+ explicit AppDownloader(const QString &storagePath, QObject *parent = 0);
+
+public slots:
+ void downloadApp(const QString &id);
+
+signals:
+ void downloadFinished(const QString &id);
+
+private slots:
+ void appJsonFetched();
+ void packageFetched();
+
+private:
+ void fetchPackage(const QString &url, const QString &storeId);
+
+ QNetworkAccessManager *m_nam;
+ QString m_storagePath;
+};
+
+#endif // APPDOWNLOADER_H
diff --git a/rockworkd/libpebble/appinfo.cpp b/rockworkd/libpebble/appinfo.cpp
new file mode 100644
index 0000000..4aeeeb7
--- /dev/null
+++ b/rockworkd/libpebble/appinfo.cpp
@@ -0,0 +1,163 @@
+#include <QSharedData>
+#include <QBuffer>
+#include <QDir>
+#include <QJsonDocument>
+#include <QUuid>
+#include "appinfo.h"
+#include "watchdatareader.h"
+#include "pebble.h"
+
+namespace {
+struct ResourceEntry {
+ int index;
+ quint32 offset;
+ quint32 length;
+ quint32 crc;
+};
+}
+
+AppInfo::AppInfo(const QString &path):
+ Bundle(path)
+{
+ if (path.isEmpty()) {
+ return;
+ }
+
+ QFile f(path + "/appinfo.json");
+ if (!f.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening appinfo.json";
+ return;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(f.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing appinfo.json";
+ return;
+ }
+
+ m_storeId = path.split("/").last();
+
+ QVariantMap map = jsonDoc.toVariant().toMap();
+
+ m_uuid = map.value("uuid").toUuid();
+ m_shortName = map.value("shortName").toString();
+ m_longName = map.value("longName").toString();
+ m_companyName = map.value("companyName").toString();
+ m_versionCode = map.value("versionCode").toInt();
+ m_versionLabel = map.value("versionLabel").toString();
+ m_capabilities = 0;
+
+ m_isWatchface = map.value("watchapp").toMap().value("watchface").toBool();
+
+ if (map.contains("appKeys")) {
+ QVariantMap appKeyMap = map.value("appKeys").toMap();
+ foreach (const QString &key, appKeyMap.keys()) {
+ m_appKeys.insert(key, appKeyMap.value(key).toInt());
+ }
+ }
+
+ if (map.contains("capabilities")) {
+ QList<QVariant> capabilities = map.value("capabilities").toList();
+
+ foreach (const QVariant &value, capabilities) {
+ QString capability = value.toString();
+ if (capability == "location") {
+ m_capabilities |= Location;
+ }
+ else if (capability == "configurable") {
+ m_capabilities |= Configurable;
+ }
+ }
+ }
+
+ QFile jsApp(path + "/pebble-js-app.js");
+ m_isJsKit = jsApp.exists();
+}
+
+AppInfo::AppInfo(const QUuid &uuid, bool isWatchFace, const QString &name, const QString &vendor, bool hasSettings):
+ m_uuid(uuid),
+ m_shortName(name),
+ m_companyName(vendor),
+ m_capabilities(hasSettings ? Configurable : None),
+ m_isWatchface(isWatchFace),
+ m_isSystemApp(true)
+{
+
+}
+
+
+AppInfo::~AppInfo()
+{}
+
+
+bool AppInfo::isValid() const
+{
+ return !m_uuid.isNull();
+}
+
+QUuid AppInfo::uuid() const
+{
+ return m_uuid;
+}
+
+QString AppInfo::storeId() const
+{
+ return m_storeId;
+}
+
+QString AppInfo::shortName() const
+{
+ return m_shortName;
+}
+
+QString AppInfo::longName() const
+{
+ return m_longName;
+}
+
+QString AppInfo::companyName() const
+{
+ return m_companyName;
+}
+
+int AppInfo::versionCode() const
+{
+ return m_versionCode;
+}
+
+QString AppInfo::versionLabel() const
+{
+ return m_versionLabel;
+}
+
+bool AppInfo::isWatchface() const
+{
+ return m_isWatchface;
+}
+
+bool AppInfo::isJSKit() const
+{
+ return m_isJsKit;
+}
+
+bool AppInfo::isSystemApp() const
+{
+ return m_isSystemApp;
+}
+
+QHash<QString, int> AppInfo::appKeys() const
+{
+ return m_appKeys;
+}
+
+bool AppInfo::hasSettings() const
+{
+ return (m_capabilities & Configurable);
+}
+
+AppInfo::Capabilities AppInfo::capabilities() const
+{
+ return m_capabilities;
+}
+
diff --git a/rockworkd/libpebble/appinfo.h b/rockworkd/libpebble/appinfo.h
new file mode 100644
index 0000000..f3bd256
--- /dev/null
+++ b/rockworkd/libpebble/appinfo.h
@@ -0,0 +1,57 @@
+#ifndef APPINFO_H
+#define APPINFO_H
+
+#include <QUuid>
+#include <QHash>
+#include <QImage>
+#include <QLoggingCategory>
+
+#include "enums.h"
+#include "bundle.h"
+
+class AppInfo: public Bundle
+{
+public:
+ enum Capability {
+ None = 0,
+ Location = 1 << 0,
+ Configurable = 1 << 2
+ };
+ Q_DECLARE_FLAGS(Capabilities, Capability)
+
+ AppInfo(const QString &path = QString());
+ AppInfo(const QUuid &uuid, bool isWatchFace, const QString &name, const QString &vendor, bool hasSettings = false);
+ ~AppInfo();
+
+ bool isValid() const;
+ QUuid uuid() const;
+ QString storeId() const;
+ QString shortName() const;
+ QString longName() const;
+ QString companyName() const;
+ int versionCode() const;
+ QString versionLabel() const;
+ bool isWatchface() const;
+ bool isJSKit() const;
+ bool isSystemApp() const;
+ QHash<QString, int> appKeys() const;
+ Capabilities capabilities() const;
+ bool hasSettings() const;
+
+private:
+ QUuid m_uuid;
+ QString m_storeId;
+ QString m_shortName;
+ QString m_longName;
+ QString m_companyName;
+ int m_versionCode = 0;
+ QString m_versionLabel;
+ QHash<QString, int> m_appKeys;
+ Capabilities m_capabilities;
+
+ bool m_isJsKit = false;
+ bool m_isWatchface = false;
+ bool m_isSystemApp = false;
+};
+
+#endif // APPINFO_H
diff --git a/rockworkd/libpebble/appmanager.cpp b/rockworkd/libpebble/appmanager.cpp
new file mode 100644
index 0000000..04a99c7
--- /dev/null
+++ b/rockworkd/libpebble/appmanager.cpp
@@ -0,0 +1,255 @@
+#include <QDir>
+#include <QSettings>
+
+#include "appmanager.h"
+#include "pebble.h"
+
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "uploadmanager.h"
+
+#include <libintl.h>
+
+#define SETTINGS_APP_UUID "07e0d9cb-8957-4bf7-9d42-35bf47caadfe"
+
+AppManager::AppManager(Pebble *pebble, WatchConnection *connection)
+ : QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ QDir dataDir(m_pebble->storagePath() + "/apps/");
+ if (!dataDir.exists() && !dataDir.mkpath(dataDir.absolutePath())) {
+ qWarning() << "could not create apps dir" << dataDir.absolutePath();
+ }
+ qDebug() << "install apps in" << dataDir.absolutePath();
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointAppFetch, this, "handleAppFetchMessage");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointSorting, this, "sortingReply");
+}
+
+QList<QUuid> AppManager::appUuids() const
+{
+ return m_appList;
+}
+
+//QList<QString> AppManager::appIds() const
+//{
+// return m_appsIds.keys();
+//}
+
+AppInfo AppManager::info(const QUuid &uuid) const
+{
+ return m_apps.value(uuid);
+}
+
+//AppInfo AppManager::info(const QString &id) const
+//{
+// return m_appsUuids.value(m_appsIds.value(id));
+//}
+
+void AppManager::rescan()
+{
+ m_appList.clear();
+ m_apps.clear();
+
+ AppInfo settingsApp(QUuid(SETTINGS_APP_UUID), false, gettext("Settings"), gettext("System app"));
+ m_appList.append(settingsApp.uuid());
+ m_apps.insert(settingsApp.uuid(), settingsApp);
+ AppInfo watchfaces(QUuid("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), false, gettext("Watchfaces"), gettext("System app"));
+ m_appList.append(watchfaces.uuid());
+ m_apps.insert(watchfaces.uuid(), watchfaces);
+ if (m_pebble->capabilities().testFlag(CapabilityHealth)) {
+ AppInfo health(QUuid("36d8c6ed-4c83-4fa1-a9e2-8f12dc941f8c"), false, gettext("Health"), gettext("System app"), true);
+ m_appList.append(health.uuid());
+ m_apps.insert(health.uuid(), health);
+ }
+ AppInfo music(QUuid("1f03293d-47af-4f28-b960-f2b02a6dd757"), false, gettext("Music"), gettext("System app"));
+ m_appList.append(music.uuid());
+ m_apps.insert(music.uuid(), music);
+ AppInfo notifications(QUuid("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), false, gettext("Notifications"), gettext("System app"));
+ m_appList.append(notifications.uuid());
+ m_apps.insert(notifications.uuid(), notifications);
+ AppInfo alarms(QUuid("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), false, gettext("Alarms"), gettext("System app"));
+ m_appList.append(alarms.uuid());
+ m_apps.insert(alarms.uuid(), alarms);
+ AppInfo ticToc(QUuid("8f3c8686-31a1-4f5f-91f5-01600c9bdc59"), true, "Tic Toc", gettext("Default watchface"));
+ m_appList.append(ticToc.uuid());
+ m_apps.insert(ticToc.uuid(), ticToc);
+
+ QDir dir(m_pebble->storagePath() + "/apps/");
+ qDebug() << "Scanning Apps dir" << dir.absolutePath();
+ Q_FOREACH(const QString &path, dir.entryList(QDir::Dirs | QDir::Readable)) {
+ QString appPath = dir.absoluteFilePath(path);
+ if (dir.exists(path + "/appinfo.json")) {
+ scanApp(appPath);
+ } else if (QFileInfo(appPath).isFile()) {
+ scanApp(appPath);
+ }
+ }
+
+ QSettings settings(m_pebble->storagePath() + "/apps.conf", QSettings::IniFormat);
+ QStringList storedList = settings.value("appList").toStringList();
+ if (storedList.isEmpty()) {
+ // User did not manually sort the app list yet... We can stop here.
+ return;
+ }
+ // Run some sanity checks
+ if (storedList.count() != m_appList.count()) {
+ qWarning() << "Installed apps not matching order config. App sort order might be wrong.";
+ return;
+ }
+ foreach (const QUuid &uuid, m_appList) {
+ if (!storedList.contains(uuid.toString())) {
+ qWarning() << "Installed apps and stored config order cannot be matched. App sort order might be wrong.";
+ return;
+ }
+ }
+ // All seems fine, repopulate m_appList
+ m_appList.clear();
+ foreach (const QString &storedId, storedList) {
+ m_appList.append(QUuid(storedId));
+ }
+}
+
+void AppManager::handleAppFetchMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ reader.read<quint8>();
+ QUuid uuid = reader.readUuid();
+ quint32 appFetchId = reader.read<quint32>();
+
+ bool haveApp = m_apps.contains(uuid);
+
+ AppFetchResponse response;
+ if (haveApp) {
+ response.setStatus(AppFetchResponse::StatusStart);
+ m_connection->writeToPebble(WatchConnection::EndpointAppFetch, response.serialize());
+ } else {
+ qWarning() << "App with uuid" << uuid.toString() << "which is not installed.";
+ response.setStatus(AppFetchResponse::StatusInvalidUUID);
+ m_connection->writeToPebble(WatchConnection::EndpointAppFetch, response.serialize());
+ emit idMismatchDetected();
+ return;
+ }
+
+ AppInfo appInfo = m_apps.value(uuid);
+
+ QString binaryFile = appInfo.file(AppInfo::FileTypeApplication, m_pebble->hardwarePlatform());
+ quint32 crc = appInfo.crc(AppInfo::FileTypeApplication, m_pebble->hardwarePlatform());
+ qDebug() << "opened binary" << binaryFile << "for hardware" << m_pebble->hardwarePlatform() << "crc" << crc;
+ m_connection->uploadManager()->uploadAppBinary(appFetchId, binaryFile, crc, [this, appInfo, appFetchId](){
+ qDebug() << "binary file uploaded successfully";
+
+ QString resourcesFile = appInfo.file(AppInfo::FileTypeResources, m_pebble->hardwarePlatform());
+ quint32 crc = appInfo.crc(AppInfo::FileTypeResources, m_pebble->hardwarePlatform());
+ qDebug() << "uploadign resource file" << resourcesFile;
+ m_connection->uploadManager()->uploadAppResources(appFetchId, resourcesFile, crc, [this, appInfo, appFetchId]() {
+ qDebug() << "resource file uploaded successfully";
+
+ QString workerFile = appInfo.file(AppInfo::FileTypeWorker, m_pebble->hardwarePlatform());
+ if (!workerFile.isEmpty()) {
+ quint32 crc = appInfo.crc(AppInfo::FileTypeWorker, m_pebble->hardwarePlatform());
+ m_connection->uploadManager()->uploadAppWorker(appFetchId, workerFile, crc, [this]() {
+ qDebug() << "worker file uploaded successfully";
+ });
+ }
+ });
+ });
+}
+
+void AppManager::sortingReply(const QByteArray &data)
+{
+ qDebug() << "have sorting reply" << data.toHex();
+}
+
+void AppManager::insertAppInfo(const AppInfo &info)
+{
+ m_appList.append(info.uuid());
+ m_apps.insert(info.uuid(), info);
+// m_appsIds.insert(info.id(), info.uuid());
+ emit appsChanged();
+}
+
+QUuid AppManager::scanApp(const QString &path)
+{
+ qDebug() << "scanning app" << path;
+ AppInfo info(path);
+ if (info.isValid()) {
+ insertAppInfo(info);
+ }
+ return info.uuid();
+}
+
+void AppManager::removeApp(const QUuid &uuid)
+{
+ m_appList.removeAll(uuid);
+ AppInfo info = m_apps.take(uuid);
+ if (!info.isValid() || info.path().isEmpty()) {
+ qWarning() << "App UUID not found. not removing";
+ return;
+
+ }
+ QDir dir(info.path());
+ dir.removeRecursively();
+ emit appsChanged();
+}
+
+void AppManager::setAppOrder(const QList<QUuid> &newList)
+{
+ // run some sanity checks
+ if (newList.count() != m_appList.count()) {
+ qWarning() << "Number of apps in order list is not matching installed apps.";
+ return;
+ }
+ foreach (const QUuid &installedUuid, m_appList) {
+ if (!newList.contains(installedUuid)) {
+ qWarning() << "App ids in order list not matching with installed apps.";
+ return;
+ }
+ }
+ if (newList.first() != QUuid(SETTINGS_APP_UUID)) {
+ qWarning() << "Settings app must be the first app.";
+ return;
+ }
+
+ m_appList = newList;
+ QSettings settings(m_pebble->storagePath() + "/apps.conf", QSettings::IniFormat);
+ QStringList tmp;
+ foreach (const QUuid &id, m_appList) {
+ tmp << id.toString();
+ }
+ settings.setValue("appList", tmp);
+ emit appsChanged();
+
+ QByteArray data;
+ WatchDataWriter writer(&data);
+ writer.write<quint8>(0x01);
+ writer.write<quint8>(m_appList.count());
+ foreach (const QUuid &uuid, m_appList) {
+ writer.writeUuid(uuid);
+ }
+
+ qDebug() << "writing" << data.toHex();
+ m_connection->writeToPebble(WatchConnection::EndpointSorting, data);
+}
+
+AppFetchResponse::AppFetchResponse(Status status):
+ m_status(status)
+{
+
+}
+
+void AppFetchResponse::setStatus(AppFetchResponse::Status status)
+{
+ m_status = status;
+}
+
+QByteArray AppFetchResponse::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.write<quint8>(m_command);
+ writer.write<quint8>(m_status);
+ return ret;
+}
diff --git a/rockworkd/libpebble/appmanager.h b/rockworkd/libpebble/appmanager.h
new file mode 100644
index 0000000..4766ebc
--- /dev/null
+++ b/rockworkd/libpebble/appmanager.h
@@ -0,0 +1,80 @@
+#ifndef APPMANAGER_H
+#define APPMANAGER_H
+
+#include <QObject>
+#include <QHash>
+#include <QUuid>
+#include "appinfo.h"
+#include "watchconnection.h"
+
+class Pebble;
+
+class AppFetchResponse: public PebblePacket
+{
+public:
+ enum Status {
+ StatusStart = 0x01,
+ StatusBusy = 0x02,
+ StatusInvalidUUID = 0x03,
+ StatusNoData = 0x04
+ };
+ AppFetchResponse(Status status = StatusNoData);
+ void setStatus(Status status);
+
+ QByteArray serialize() const override;
+
+private:
+ quint8 m_command = 1; // I guess there's only one command for now
+ Status m_status = StatusNoData;
+};
+
+class AppManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum Action {
+ ActionGetAppBankStatus = 1,
+ ActionRemoveApp = 2,
+ ActionRefreshApp = 3,
+ ActionGetAppBankUuids = 5
+ };
+
+ explicit AppManager(Pebble *pebble, WatchConnection *connection);
+
+ QList<QUuid> appUuids() const;
+
+ AppInfo info(const QUuid &uuid) const;
+
+ void insertAppInfo(const AppInfo &info);
+
+ QUuid scanApp(const QString &path);
+
+ void removeApp(const QUuid &uuid);
+
+ void setAppOrder(const QList<QUuid> &newList);
+
+public slots:
+ void rescan();
+
+private slots:
+ void handleAppFetchMessage(const QByteArray &data);
+ void sortingReply(const QByteArray &data);
+
+signals:
+ void appsChanged();
+
+ void uploadRequested(const QString &file, quint32 appInstallId);
+
+ void idMismatchDetected();
+
+private:
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ QList<QUuid> m_appList;
+ QHash<QUuid, AppInfo> m_apps;
+};
+
+#endif // APPMANAGER_H
diff --git a/rockworkd/libpebble/appmetadata.cpp b/rockworkd/libpebble/appmetadata.cpp
new file mode 100644
index 0000000..5aa423f
--- /dev/null
+++ b/rockworkd/libpebble/appmetadata.cpp
@@ -0,0 +1,73 @@
+#include "appmetadata.h"
+
+#include "watchdatawriter.h"
+
+AppMetadata::AppMetadata()
+{
+
+}
+
+QUuid AppMetadata::uuid() const
+{
+ return m_uuid;
+}
+
+void AppMetadata::setUuid(const QUuid &uuid)
+{
+ m_uuid = uuid;
+}
+
+void AppMetadata::setFlags(quint32 flags)
+{
+ m_flags = flags;
+}
+
+void AppMetadata::setIcon(quint32 icon)
+{
+ m_icon = icon;
+}
+
+void AppMetadata::setAppVersion(quint8 appVersionMajor, quint8 appVersionMinor)
+{
+ m_appVersionMajor = appVersionMajor;
+ m_appVersionMinor = appVersionMinor;
+}
+
+void AppMetadata::setSDKVersion(quint8 sdkVersionMajor, quint8 sdkVersionMinor)
+{
+ m_sdkVersionMajor = sdkVersionMajor;
+ m_sdkVersionMinor = sdkVersionMinor;
+}
+
+void AppMetadata::setAppFaceBgColor(quint8 color)
+{
+ m_appFaceBgColor = color;
+}
+
+void AppMetadata::setAppFaceTemplateId(quint8 templateId)
+{
+ m_appFaceTemplateId = templateId;
+}
+
+void AppMetadata::setAppName(const QString &appName)
+{
+ m_appName = appName;
+}
+
+QByteArray AppMetadata::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.writeUuid(m_uuid);
+ writer.writeLE<quint32>(m_flags);
+ writer.writeLE<quint32>(m_icon);
+ writer.writeLE<quint8>(m_appVersionMajor);
+ writer.writeLE<quint8>(m_appVersionMinor);
+ writer.writeLE<quint8>(m_sdkVersionMajor);
+ writer.writeLE<quint8>(m_sdkVersionMinor);
+ writer.writeLE<quint8>(m_appFaceBgColor);
+ writer.writeLE<quint8>(m_appFaceTemplateId);
+ writer.writeFixedString(96, m_appName);
+ return ret;
+}
+
diff --git a/rockworkd/libpebble/appmetadata.h b/rockworkd/libpebble/appmetadata.h
new file mode 100644
index 0000000..6583c68
--- /dev/null
+++ b/rockworkd/libpebble/appmetadata.h
@@ -0,0 +1,39 @@
+#ifndef APPMETADATA_H
+#define APPMETADATA_H
+
+#include "watchconnection.h"
+
+class AppMetadata: public PebblePacket
+{
+public:
+ AppMetadata();
+
+ QUuid uuid() const;
+ void setUuid(const QUuid &uuid);
+ void setFlags(quint32 flags);
+ void setIcon(quint32 icon);
+ void setAppVersion(quint8 appVersionMajor, quint8 appVersionMinor);
+ void setSDKVersion(quint8 sdkVersionMajor, quint8 sdkVersionMinor);
+ void setAppFaceBgColor(quint8 color);
+ void setAppFaceTemplateId(quint8 templateId);
+ void setAppName(const QString &appName);
+
+ QByteArray serialize() const;
+signals:
+
+public slots:
+
+private:
+ QUuid m_uuid;
+ quint32 m_flags;
+ quint32 m_icon;
+ quint8 m_appVersionMajor;
+ quint8 m_appVersionMinor;
+ quint8 m_sdkVersionMajor;
+ quint8 m_sdkVersionMinor;
+ quint8 m_appFaceBgColor;
+ quint8 m_appFaceTemplateId;
+ QString m_appName; // fixed, 96
+};
+
+#endif // APPMETADATA_H
diff --git a/rockworkd/libpebble/appmsgmanager.cpp b/rockworkd/libpebble/appmsgmanager.cpp
new file mode 100644
index 0000000..e20c8d0
--- /dev/null
+++ b/rockworkd/libpebble/appmsgmanager.cpp
@@ -0,0 +1,461 @@
+#include <QTimer>
+
+#include "pebble.h"
+#include "appmsgmanager.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+// TODO D-Bus server for non JS kit apps!!!!
+
+AppMsgManager::AppMsgManager(Pebble *pebble, AppManager *apps, WatchConnection *connection)
+ : QObject(pebble),
+ m_pebble(pebble),
+ apps(apps),
+ m_connection(connection), _lastTransactionId(0), _timeout(new QTimer(this))
+{
+ connect(m_connection, &WatchConnection::watchConnected,
+ this, &AppMsgManager::handleWatchConnectedChanged);
+
+ _timeout->setSingleShot(true);
+ _timeout->setInterval(3000);
+ connect(_timeout, &QTimer::timeout,
+ this, &AppMsgManager::handleTimeout);
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointLauncher, this, "handleLauncherMessage");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointAppLaunch, this, "handleAppLaunchMessage");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointApplicationMessage, this, "handleApplicationMessage");
+}
+
+void AppMsgManager::handleLauncherMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint8 messageType = reader.read<quint8>();
+ switch (messageType) {
+ case AppMessagePush:
+ handleLauncherPushMessage(data);
+ break;
+
+ // TODO we ignore those for now.
+ case AppMessageAck:
+ qDebug() << "Watch accepted application launch";
+ break;
+ case AppMessageNack:
+ qDebug() << "Watch denied application launch";
+ break;
+ case AppMessageRequest:
+ qWarning() << "Unhandled Launcher message (AppMessagePush)";
+ break;
+ }
+}
+
+void AppMsgManager::handleApplicationMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint8 messageType = reader.read<quint8>();
+ switch (messageType) {
+ case AppMessagePush:
+ handlePushMessage(data);
+ break;
+ case AppMessageAck:
+ handleAckMessage(data, true);
+ break;
+ case AppMessageNack:
+ handleAckMessage(data, false);
+ break;
+ default:
+ qWarning() << "Unknown application message type:" << int(data.at(0));
+ break;
+ }
+}
+
+void AppMsgManager::send(const QUuid &uuid, const QVariantMap &data, const std::function<void ()> &ackCallback, const std::function<void ()> &nackCallback)
+{
+ PendingTransaction trans;
+ trans.uuid = uuid;
+ trans.transactionId = ++_lastTransactionId;
+ trans.dict = mapAppKeys(uuid, data);
+ trans.ackCallback = ackCallback;
+ trans.nackCallback = nackCallback;
+
+ qDebug() << "Queueing appmsg" << trans.transactionId << "to" << trans.uuid
+ << "with dict" << trans.dict;
+
+ _pending.enqueue(trans);
+ if (_pending.size() == 1) {
+ // This is the only transaction on the queue
+ // Therefore, we were idle before: we can submit this transaction right now.
+ transmitNextPendingTransaction();
+ }
+}
+
+void AppMsgManager::setMessageHandler(const QUuid &uuid, MessageHandlerFunc func)
+{
+ _handlers.insert(uuid, func);
+}
+
+void AppMsgManager::clearMessageHandler(const QUuid &uuid)
+{
+ _handlers.remove(uuid);
+}
+
+uint AppMsgManager::lastTransactionId() const
+{
+ return _lastTransactionId;
+}
+
+uint AppMsgManager::nextTransactionId() const
+{
+ return _lastTransactionId + 1;
+}
+
+void AppMsgManager::send(const QUuid &uuid, const QVariantMap &data)
+{
+ std::function<void()> nullCallback;
+ send(uuid, data, nullCallback, nullCallback);
+}
+
+void AppMsgManager::launchApp(const QUuid &uuid)
+{
+ if (m_pebble->softwareVersion() < "v3.0") {
+ WatchConnection::Dict dict;
+ dict.insert(1, LauncherActionStart);
+
+ qDebug() << "Sending start message to launcher" << uuid << dict;
+ QByteArray msg = buildPushMessage(++_lastTransactionId, uuid, dict);
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, msg);
+ }
+ else {
+ QByteArray msg = buildLaunchMessage(LauncherActionStart, uuid);
+ qDebug() << "Sending start message to launcher" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointAppLaunch, msg);
+ }
+}
+
+void AppMsgManager::closeApp(const QUuid &uuid)
+{
+ if (m_pebble->softwareVersion() < "v3.0") {
+ WatchConnection::Dict dict;
+ dict.insert(1, LauncherActionStop);
+
+ qDebug() << "Sending stop message to launcher" << uuid << dict;
+ QByteArray msg = buildPushMessage(++_lastTransactionId, uuid, dict);
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, msg);
+ }
+ else {
+ QByteArray msg = buildLaunchMessage(LauncherActionStop, uuid);
+ qDebug() << "Sending stop message to launcher" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointAppLaunch, msg);
+ }
+}
+
+WatchConnection::Dict AppMsgManager::mapAppKeys(const QUuid &uuid, const QVariantMap &data)
+{
+ AppInfo info = apps->info(uuid);
+ if (info.uuid() != uuid) {
+ qWarning() << "Unknown app GUID while sending message:" << uuid;
+ }
+
+ WatchConnection::Dict d;
+
+ qDebug() << "Have appkeys:" << info.appKeys().keys();
+
+ for (QVariantMap::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) {
+ if (info.appKeys().contains(it.key())) {
+ d.insert(info.appKeys().value(it.key()), it.value());
+ } else {
+ // Even if we do not know about this appkey, try to see if it's already a numeric key we
+ // can send to the watch.
+ bool ok = false;
+ int num = it.key().toInt(&ok);
+ if (ok) {
+ d.insert(num, it.value());
+ } else {
+ qWarning() << "Unknown appKey" << it.key() << "for app with GUID" << uuid;
+ }
+ }
+ }
+
+ return d;
+}
+
+QVariantMap AppMsgManager::mapAppKeys(const QUuid &uuid, const WatchConnection::Dict &dict)
+{
+ AppInfo info = apps->info(uuid);
+ if (info.uuid() != uuid) {
+ qWarning() << "Unknown app GUID while sending message:" << uuid;
+ }
+
+ QVariantMap data;
+
+ for (WatchConnection::Dict::const_iterator it = dict.constBegin(); it != dict.constEnd(); ++it) {
+ qDebug() << "checking app key" << it.key() << info.appKeys().key(it.key());
+ if (info.appKeys().values().contains(it.key())) {
+ data.insert(info.appKeys().key(it.key()), it.value());
+ } else {
+ qWarning() << "Unknown appKey value" << it.key() << "for app with GUID" << uuid;
+ data.insert(QString::number(it.key()), it.value());
+ }
+ }
+
+ return data;
+}
+
+bool AppMsgManager::unpackAppLaunchMessage(const QByteArray &msg, QUuid *uuid)
+{
+ WatchDataReader reader(msg);
+ quint8 action = reader.read<quint8>();
+ Q_UNUSED(action);
+
+ *uuid = reader.readUuid();
+
+ if (reader.bad()) {
+ return false;
+ }
+
+ return true;
+}
+
+bool AppMsgManager::unpackPushMessage(const QByteArray &msg, quint8 *transaction, QUuid *uuid, WatchConnection::Dict *dict)
+{
+ WatchDataReader reader(msg);
+ quint8 code = reader.read<quint8>();
+ Q_UNUSED(code);
+ Q_ASSERT(code == AppMessagePush);
+
+ *transaction = reader.read<quint8>();
+ *uuid = reader.readUuid();
+ *dict = reader.readDict();
+
+ if (reader.bad()) {
+ return false;
+ }
+
+ return true;
+}
+
+QByteArray AppMsgManager::buildPushMessage(quint8 transaction, const QUuid &uuid, const WatchConnection::Dict &dict)
+{
+ QByteArray ba;
+ WatchDataWriter writer(&ba);
+ writer.write<quint8>(AppMessagePush);
+ writer.write<quint8>(transaction);
+ writer.writeUuid(uuid);
+ writer.writeDict(dict);
+
+ return ba;
+}
+
+QByteArray AppMsgManager::buildLaunchMessage(quint8 messageType, const QUuid &uuid)
+{
+ QByteArray ba;
+ WatchDataWriter writer(&ba);
+ writer.write<quint8>(messageType);
+ writer.writeUuid(uuid);
+
+ return ba;
+}
+
+QByteArray AppMsgManager::buildAckMessage(quint8 transaction)
+{
+ QByteArray ba(2, Qt::Uninitialized);
+ ba[0] = AppMessageAck;
+ ba[1] = transaction;
+ return ba;
+}
+
+QByteArray AppMsgManager::buildNackMessage(quint8 transaction)
+{
+ QByteArray ba(2, Qt::Uninitialized);
+ ba[0] = AppMessageNack;
+ ba[1] = transaction;
+ return ba;
+}
+
+void AppMsgManager::handleAppLaunchMessage(const QByteArray &data)
+{
+ QUuid uuid;
+ if (!unpackAppLaunchMessage(data, &uuid)) {
+ qWarning() << "Failed to parse App Launch message";
+ return;
+ }
+
+ switch (data.at(0)) {
+ case LauncherActionStart:
+ qDebug() << "App starting in watch:" << uuid;
+ emit appStarted(uuid);
+ break;
+ case LauncherActionStop:
+ qDebug() << "App stopping in watch:" << uuid;
+ emit appStopped(uuid);
+ break;
+ default:
+ qWarning() << "App Launch pushed unknown message:" << uuid;
+ break;
+ }
+}
+
+void AppMsgManager::handleLauncherPushMessage(const QByteArray &data)
+{
+ quint8 transaction;
+ QUuid uuid;
+ WatchConnection::Dict dict;
+
+ if (!unpackPushMessage(data, &transaction, &uuid, &dict)) {
+ // Failed to parse!
+ // Since we're the only one handling this endpoint,
+ // all messages must be accepted
+ qWarning() << "Failed to parser LAUNCHER PUSH message";
+ return;
+ }
+ qDebug() << "have launcher push message" << data.toHex() << dict.keys();
+ if (!dict.contains(1)) {
+ qWarning() << "LAUNCHER message has no item in dict";
+ return;
+ }
+
+ switch (dict.value(1).toInt()) {
+ case LauncherActionStart:
+ qDebug() << "App starting in watch:" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, buildAckMessage(transaction));
+ emit appStarted(uuid);
+ break;
+ case LauncherActionStop:
+ qDebug() << "App stopping in watch:" << uuid;
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, buildAckMessage(transaction));
+ emit appStopped(uuid);
+ break;
+ default:
+ qWarning() << "LAUNCHER pushed unknown message:" << uuid << dict;
+ m_connection->writeToPebble(WatchConnection::EndpointLauncher, buildNackMessage(transaction));
+ break;
+ }
+}
+
+void AppMsgManager::handlePushMessage(const QByteArray &data)
+{
+ quint8 transaction;
+ QUuid uuid;
+ WatchConnection::Dict dict;
+
+ if (!unpackPushMessage(data, &transaction, &uuid, &dict)) {
+ qWarning() << "Failed to parse APP_MSG PUSH";
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, buildNackMessage(transaction));
+ return;
+ }
+
+ qDebug() << "Received appmsg PUSH from" << uuid << "with" << dict;
+
+ QVariantMap msg = mapAppKeys(uuid, dict);
+ qDebug() << "Mapped dict" << msg;
+
+ bool result;
+
+ MessageHandlerFunc handler = _handlers.value(uuid);
+ if (handler) {
+ result = handler(msg);
+ } else {
+ // No handler? Let's just send an ACK.
+ result = false;
+ }
+
+ if (result) {
+ qDebug() << "ACKing transaction" << transaction;
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, buildAckMessage(transaction));
+ } else {
+ qDebug() << "NACKing transaction" << transaction;
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, buildNackMessage(transaction));
+ }
+}
+
+void AppMsgManager::handleAckMessage(const QByteArray &data, bool ack)
+{
+ if (data.size() < 2) {
+ qWarning() << "invalid ack/nack message size";
+ return;
+ }
+
+ const quint8 type = data[0]; Q_UNUSED(type);
+ const quint8 recv_transaction = data[1];
+
+ Q_ASSERT(type == AppMessageAck || type == AppMessageNack);
+
+ if (_pending.empty()) {
+ qWarning() << "received an ack/nack for transaction" << recv_transaction << "but no transaction is pending";
+ return;
+ }
+
+ PendingTransaction &trans = _pending.head();
+ if (trans.transactionId != recv_transaction) {
+ qWarning() << "received an ack/nack but for the wrong transaction";
+ }
+
+ qDebug() << "Got " << (ack ? "ACK" : "NACK") << " to transaction" << trans.transactionId;
+
+ _timeout->stop();
+
+ if (ack) {
+ if (trans.ackCallback) {
+ trans.ackCallback();
+ }
+ } else {
+ if (trans.nackCallback) {
+ trans.nackCallback();
+ }
+ }
+
+ _pending.dequeue();
+
+ if (!_pending.empty()) {
+ transmitNextPendingTransaction();
+ }
+}
+
+void AppMsgManager::handleWatchConnectedChanged()
+{
+ // If the watch is disconnected, everything breaks loose
+ // TODO In the future we may want to avoid doing the following.
+ if (!m_connection->isConnected()) {
+ abortPendingTransactions();
+ }
+}
+
+void AppMsgManager::handleTimeout()
+{
+ // Abort the first transaction
+ Q_ASSERT(!_pending.empty());
+ PendingTransaction trans = _pending.dequeue();
+
+ qWarning() << "timeout on appmsg transaction" << trans.transactionId;
+
+ if (trans.nackCallback) {
+ trans.nackCallback();
+ }
+
+ if (!_pending.empty()) {
+ transmitNextPendingTransaction();
+ }
+}
+
+void AppMsgManager::transmitNextPendingTransaction()
+{
+ Q_ASSERT(!_pending.empty());
+ PendingTransaction &trans = _pending.head();
+
+ QByteArray msg = buildPushMessage(trans.transactionId, trans.uuid, trans.dict);
+
+ m_connection->writeToPebble(WatchConnection::EndpointApplicationMessage, msg);
+
+ _timeout->start();
+}
+
+void AppMsgManager::abortPendingTransactions()
+{
+ // Invoke all the NACK callbacks in the pending queue, then drop them.
+ Q_FOREACH(const PendingTransaction &trans, _pending) {
+ if (trans.nackCallback) {
+ trans.nackCallback();
+ }
+ }
+
+ _pending.clear();
+}
diff --git a/rockworkd/libpebble/appmsgmanager.h b/rockworkd/libpebble/appmsgmanager.h
new file mode 100644
index 0000000..77ee480
--- /dev/null
+++ b/rockworkd/libpebble/appmsgmanager.h
@@ -0,0 +1,94 @@
+#ifndef APPMSGMANAGER_H
+#define APPMSGMANAGER_H
+
+#include <functional>
+#include <QUuid>
+#include <QQueue>
+
+#include "watchconnection.h"
+#include "appmanager.h"
+
+class AppMsgManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum AppMessage {
+ AppMessagePush = 1,
+ AppMessageRequest = 2,
+ AppMessageAck = 0xFF,
+ AppMessageNack = 0x7F
+ };
+ enum LauncherMessage {
+ LauncherActionStart = 1,
+ LauncherActionStop = 0
+ };
+
+ explicit AppMsgManager(Pebble *pebble, AppManager *apps, WatchConnection *connection);
+
+ void send(const QUuid &uuid, const QVariantMap &data,
+ const std::function<void()> &ackCallback,
+ const std::function<void()> &nackCallback);
+
+ typedef std::function<bool(const QVariantMap &)> MessageHandlerFunc;
+ void setMessageHandler(const QUuid &uuid, MessageHandlerFunc func);
+ void clearMessageHandler(const QUuid &uuid);
+
+ uint lastTransactionId() const;
+ uint nextTransactionId() const;
+
+public slots:
+ void send(const QUuid &uuid, const QVariantMap &data);
+ void launchApp(const QUuid &uuid);
+ void closeApp(const QUuid &uuid);
+
+signals:
+ void appStarted(const QUuid &uuid);
+ void appStopped(const QUuid &uuid);
+
+private:
+ WatchConnection::Dict mapAppKeys(const QUuid &uuid, const QVariantMap &data);
+ QVariantMap mapAppKeys(const QUuid &uuid, const WatchConnection::Dict &dict);
+
+ static bool unpackAppLaunchMessage(const QByteArray &msg, QUuid *uuid);
+ static bool unpackPushMessage(const QByteArray &msg, quint8 *transaction, QUuid *uuid, WatchConnection::Dict *dict);
+
+ static QByteArray buildPushMessage(quint8 transaction, const QUuid &uuid, const WatchConnection::Dict &dict);
+ static QByteArray buildLaunchMessage(quint8 messageType, const QUuid &uuid);
+ static QByteArray buildAckMessage(quint8 transaction);
+ static QByteArray buildNackMessage(quint8 transaction);
+
+ void handleLauncherPushMessage(const QByteArray &data);
+ void handlePushMessage(const QByteArray &data);
+ void handleAckMessage(const QByteArray &data, bool ack);
+
+ void transmitNextPendingTransaction();
+ void abortPendingTransactions();
+
+private slots:
+ void handleWatchConnectedChanged();
+ void handleTimeout();
+
+ void handleAppLaunchMessage(const QByteArray &data);
+ void handleLauncherMessage(const QByteArray &data);
+ void handleApplicationMessage(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ AppManager *apps;
+ WatchConnection *m_connection;
+ QHash<QUuid, MessageHandlerFunc> _handlers;
+ quint8 _lastTransactionId;
+
+ struct PendingTransaction {
+ quint8 transactionId;
+ QUuid uuid;
+ WatchConnection::Dict dict;
+ std::function<void()> ackCallback;
+ std::function<void()> nackCallback;
+ };
+ QQueue<PendingTransaction> _pending;
+ QTimer *_timeout;
+};
+
+#endif // APPMSGMANAGER_H
diff --git a/rockworkd/libpebble/blobdb.cpp b/rockworkd/libpebble/blobdb.cpp
new file mode 100644
index 0000000..e5a2f77
--- /dev/null
+++ b/rockworkd/libpebble/blobdb.cpp
@@ -0,0 +1,584 @@
+#include "blobdb.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+#include <QDebug>
+#include <QOrganizerRecurrenceRule>
+#include <QDir>
+#include <QSettings>
+
+BlobDB::BlobDB(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointBlobDB, this, "blobCommandReply");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointActionHandler, this, "actionInvoked");
+
+ connect(m_connection, &WatchConnection::watchConnected, [this]() {
+ if (m_currentCommand) {
+ delete m_currentCommand;
+ m_currentCommand = nullptr;
+ }
+ });
+
+ m_blobDBStoragePath = m_pebble->storagePath() + "/blobdb/";
+ QDir dir(m_blobDBStoragePath);
+ if (!dir.exists() && !dir.mkpath(m_blobDBStoragePath)) {
+ qWarning() << "Error creating blobdb storage dir.";
+ return;
+ }
+ dir.setNameFilters({"calendarevent-*"});
+ foreach (const QFileInfo &fi, dir.entryInfoList()) {
+ CalendarEvent event;
+ event.loadFromCache(m_blobDBStoragePath, fi.fileName().right(QUuid().toString().length()));
+
+ m_calendarEntries.append(event);
+ }
+}
+
+void BlobDB::insertNotification(const Notification &notification)
+{
+ TimelineAttribute::IconID iconId = TimelineAttribute::IconIDDefaultBell;
+ TimelineAttribute::Color color = TimelineAttribute::ColorRed;
+ QString muteName;
+ switch (notification.type()) {
+ case Notification::NotificationTypeAlarm:
+ iconId = TimelineAttribute::IconIDAlarm;
+ muteName = "Alarms";
+ break;
+ case Notification::NotificationTypeFacebook:
+ iconId = TimelineAttribute::IconIDFacebook;
+ color = TimelineAttribute::ColorBlue;
+ muteName = "facebook";
+ break;
+ case Notification::NotificationTypeGMail:
+ iconId = TimelineAttribute::IconIDGMail;
+ muteName = "GMail";
+ break;
+ case Notification::NotificationTypeHangout:
+ iconId = TimelineAttribute::IconIDHangout;
+ color = TimelineAttribute::ColorGreen;
+ muteName = "Hangout";
+ break;
+ case Notification::NotificationTypeMissedCall:
+ iconId = TimelineAttribute::IconIDDefaultMissedCall;
+ muteName = "call notifications";
+ break;
+ case Notification::NotificationTypeMusic:
+ iconId = TimelineAttribute::IconIDMusic;
+ muteName = "music";
+ break;
+ case Notification::NotificationTypeReminder:
+ iconId = TimelineAttribute::IconIDReminder;
+ muteName = "reminders";
+ break;
+ case Notification::NotificationTypeTelegram:
+ iconId = TimelineAttribute::IconIDTelegram;
+ color = TimelineAttribute::ColorLightBlue;
+ muteName = "Telegram";
+ break;
+ case Notification::NotificationTypeTwitter:
+ iconId = TimelineAttribute::IconIDTwitter;
+ color = TimelineAttribute::ColorBlue2;
+ muteName = "Twitter";
+ break;
+ case Notification::NotificationTypeWeather:
+ iconId = TimelineAttribute::IconIDWeather;
+ muteName = "Weather";
+ break;
+ case Notification::NotificationTypeWhatsApp:
+ iconId = TimelineAttribute::IconIDWhatsApp;
+ color = TimelineAttribute::ColorGreen;
+ muteName = "WhatsApp";
+ break;
+ case Notification::NotificationTypeSMS:
+ muteName = "SMS";
+ iconId = TimelineAttribute::IconIDDefaultBell;
+ break;
+ case Notification::NotificationTypeEmail:
+ default:
+ muteName = "e mails";
+ iconId = TimelineAttribute::IconIDDefaultBell;
+ break;
+ }
+
+ QUuid itemUuid = QUuid::createUuid();
+ TimelineItem timelineItem(itemUuid, TimelineItem::TypeNotification);
+ timelineItem.setFlags(TimelineItem::FlagSingleEvent);
+
+ TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, notification.sender().left(64).toUtf8());
+ timelineItem.appendAttribute(titleAttribute);
+
+ TimelineAttribute subjectAttribute(TimelineAttribute::TypeSubtitle, notification.subject().left(64).toUtf8());
+ timelineItem.appendAttribute(subjectAttribute);
+
+ TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, notification.body().toUtf8());
+ timelineItem.appendAttribute(bodyAttribute);
+
+ TimelineAttribute iconAttribute(TimelineAttribute::TypeTinyIcon, iconId);
+ timelineItem.appendAttribute(iconAttribute);
+
+ TimelineAttribute colorAttribute(TimelineAttribute::TypeColor, color);
+ timelineItem.appendAttribute(colorAttribute);
+
+ TimelineAction dismissAction(0, TimelineAction::TypeDismiss);
+ TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss");
+ dismissAction.appendAttribute(dismissAttribute);
+ timelineItem.appendAction(dismissAction);
+
+ TimelineAction muteAction(1, TimelineAction::TypeGeneric);
+ TimelineAttribute muteActionAttribute(TimelineAttribute::TypeTitle, "Mute " + muteName.toUtf8());
+ muteAction.appendAttribute(muteActionAttribute);
+ timelineItem.appendAction(muteAction);
+
+ if (!notification.actToken().isEmpty()) {
+ TimelineAction actAction(2, TimelineAction::TypeGeneric);
+ TimelineAttribute actActionAttribute(TimelineAttribute::TypeTitle, "Open on phone");
+ actAction.appendAttribute(actActionAttribute);
+ timelineItem.appendAction(actAction);
+ }
+
+ insert(BlobDB::BlobDBIdNotification, timelineItem);
+ m_notificationSources.insert(itemUuid, notification);
+}
+
+void BlobDB::insertTimelinePin(const QUuid &uuid, TimelineItem::Layout layout, const QDateTime &startTime, const QDateTime &endTime, const QString &title, const QString &desctiption, const QMap<QString, QString> fields, bool recurring)
+{
+// TimelineItem item(TimelineItem::TypePin, TimelineItem::FlagSingleEvent, QDateTime::currentDateTime().addMSecs(1000 * 60 * 2), 60);
+
+ qDebug() << "inserting timeline pin:" << title << startTime << endTime;
+ int duration = (endTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch()) / 1000 / 60;
+ TimelineItem item(uuid, TimelineItem::TypePin, TimelineItem::FlagSingleEvent, startTime, duration);
+ item.setLayout(layout);
+
+ TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, title.toUtf8());
+ item.appendAttribute(titleAttribute);
+
+ if (!desctiption.isEmpty()) {
+ TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, desctiption.left(128).toUtf8());
+ item.appendAttribute(bodyAttribute);
+ }
+
+// TimelineAttribute iconAttribute(TimelineAttribute::TypeTinyIcon, TimelineAttribute::IconIDTelegram);
+// item.appendAttribute(iconAttribute);
+
+ if (!fields.isEmpty()) {
+ TimelineAttribute fieldNames(TimelineAttribute::TypeFieldNames, fields.keys());
+ item.appendAttribute(fieldNames);
+
+ TimelineAttribute fieldValues(TimelineAttribute::TypeFieldValues, fields.values());
+ item.appendAttribute(fieldValues);
+ }
+
+ if (recurring) {
+ TimelineAttribute guess(TimelineAttribute::TypeRecurring, 0x01);
+ item.appendAttribute(guess);
+ }
+
+ TimelineAction dismissAction(0, TimelineAction::TypeDismiss);
+ TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss");
+ dismissAction.appendAttribute(dismissAttribute);
+ item.appendAction(dismissAction);
+
+ insert(BlobDB::BlobDBIdPin, item);
+}
+
+void BlobDB::removeTimelinePin(const QUuid &uuid)
+{
+ qDebug() << "Removing timeline pin:" << uuid;
+ remove(BlobDBId::BlobDBIdPin, uuid);
+}
+
+void BlobDB::insertReminder()
+{
+
+ TimelineItem item(TimelineItem::TypeReminder, TimelineItem::FlagSingleEvent, QDateTime::currentDateTime().addMSecs(1000 * 60 * 2), 0);
+
+ TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, "ReminderTitle");
+ item.appendAttribute(titleAttribute);
+
+ TimelineAttribute subjectAttribute(TimelineAttribute::TypeSubtitle, "ReminderSubtitle");
+ item.appendAttribute(subjectAttribute);
+
+ TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, "ReminderBody");
+ item.appendAttribute(bodyAttribute);
+
+ QByteArray data;
+ data.append(0x07); data.append('\0'); data.append('\0'); data.append(0x80);
+ TimelineAttribute guessAttribute(TimelineAttribute::TypeTinyIcon, data);
+ item.appendAttribute(guessAttribute);
+ qDebug() << "attrib" << guessAttribute.serialize();
+
+ TimelineAction dismissAction(0, TimelineAction::TypeDismiss);
+ TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss");
+ dismissAction.appendAttribute(dismissAttribute);
+ item.appendAction(dismissAction);
+
+ insert(BlobDB::BlobDBIdReminder, item);
+ // qDebug() << "adding timeline item" << ddd.toHex();
+
+}
+
+void BlobDB::clearTimeline()
+{
+ foreach (CalendarEvent entry, m_calendarEntries) {
+ entry.removeFromCache(m_blobDBStoragePath);
+ }
+ m_calendarEntries.clear();
+ clear(BlobDB::BlobDBIdPin);
+}
+
+void BlobDB::syncCalendar(const QList<CalendarEvent> &events)
+{
+ qDebug() << "BlobDB: Starting calendar sync for" << events.count() << "entries";
+ QList<CalendarEvent> itemsToSync;
+ QList<CalendarEvent> itemsToAdd;
+ QList<CalendarEvent> itemsToDelete;
+
+ // Filter out invalid items
+ foreach (const CalendarEvent &event, events) {
+ if (event.startTime().isValid() && event.endTime().isValid()
+ && event.startTime().addDays(2) > QDateTime::currentDateTime()
+ && QDateTime::currentDateTime().addDays(5) > event.startTime()) {
+ itemsToSync.append(event);
+ }
+ }
+
+ // Compare events to local ones
+ foreach (const CalendarEvent &event, itemsToSync) {
+ CalendarEvent syncedEvent = findCalendarEvent(event.id());
+ if (!syncedEvent.isValid()) {
+ itemsToAdd.append(event);
+ } else if (!(syncedEvent == event)) {
+ qDebug() << "event has changed!";
+ itemsToDelete.append(syncedEvent);
+ itemsToAdd.append(event);
+ }
+ }
+
+ // Find stale local ones
+ foreach (const CalendarEvent &event, m_calendarEntries) {
+ bool found = false;
+ foreach (const CalendarEvent &tmp, events) {
+ if (tmp.id() == event.id()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ qDebug() << "removing stale timeline entry";
+ itemsToDelete.append(event);
+ }
+ }
+
+ foreach (const CalendarEvent &event, itemsToDelete) {
+ removeTimelinePin(event.uuid());
+ m_calendarEntries.removeAll(event);
+ event.removeFromCache(m_blobDBStoragePath);
+ }
+
+ qDebug() << "adding" << itemsToAdd.count() << "timeline entries";
+ foreach (const CalendarEvent &event, itemsToAdd) {
+ QMap<QString, QString> fields;
+ if (!event.location().isEmpty()) fields.insert("Location", event.location());
+ if (!event.calendar().isEmpty()) fields.insert("Calendar", event.calendar());
+ if (!event.comment().isEmpty()) fields.insert("Comments", event.comment());
+ if (!event.guests().isEmpty()) fields.insert("Guests", event.guests().join(", "));
+ insertTimelinePin(event.uuid(), TimelineItem::LayoutCalendar, event.startTime(), event.endTime(), event.title(), event.description(), fields, event.recurring());
+ m_calendarEntries.append(event);
+ event.saveToCache(m_blobDBStoragePath);
+ }
+}
+
+void BlobDB::clearApps()
+{
+ clear(BlobDBId::BlobDBIdApp);
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ s.remove("");
+}
+
+void BlobDB::insertAppMetaData(const AppInfo &info)
+{
+ if (!m_pebble->connected()) {
+ qWarning() << "Pebble is not connected. Cannot install app";
+ return;
+ }
+
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ if (s.value(info.uuid().toString(), false).toBool()) {
+ qWarning() << "App already in DB. Not syncing again";
+ return;
+ }
+
+ AppMetadata metaData = appInfoToMetadata(info, m_pebble->hardwarePlatform());
+
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = BlobDBIdApp;
+
+ cmd->m_key = metaData.uuid().toRfc4122();
+ cmd->m_value = metaData.serialize();
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::removeApp(const AppInfo &info)
+{
+ remove(BlobDBId::BlobDBIdApp, info.uuid());
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ s.remove(info.uuid().toString());
+}
+
+void BlobDB::insert(BlobDBId database, const TimelineItem &item)
+{
+ if (!m_connection->isConnected()) {
+ return;
+ }
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = database;
+
+ cmd->m_key = item.itemId().toRfc4122();
+ cmd->m_value = item.serialize();
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::remove(BlobDB::BlobDBId database, const QUuid &uuid)
+{
+ if (!m_connection->isConnected()) {
+ return;
+ }
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationDelete;
+ cmd->m_token = generateToken();
+ cmd->m_database = database;
+
+ cmd->m_key = uuid.toRfc4122();
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::clear(BlobDB::BlobDBId database)
+{
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationClear;
+ cmd->m_token = generateToken();
+ cmd->m_database = database;
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::setHealthParams(const HealthParams &healthParams)
+{
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = BlobDBIdAppSettings;
+
+ cmd->m_key = "activityPreferences";
+ cmd->m_value = healthParams.serialize();
+
+ qDebug() << "Setting health params. Enabled:" << healthParams.enabled() << cmd->serialize().toHex();
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::setUnits(bool imperial)
+{
+ BlobCommand *cmd = new BlobCommand();
+ cmd->m_command = BlobDB::OperationInsert;
+ cmd->m_token = generateToken();
+ cmd->m_database = BlobDBIdAppSettings;
+
+ cmd->m_key = "unitsDistance";
+ WatchDataWriter writer(&cmd->m_value);
+ writer.write<quint8>(imperial ? 0x01 : 0x00);
+
+ m_commandQueue.append(cmd);
+ sendNext();
+}
+
+void BlobDB::blobCommandReply(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint16 token = reader.readLE<quint16>();
+ quint8 status = reader.read<quint8>();
+ if (m_currentCommand->m_token != token) {
+ qWarning() << "Received reply for unexpected token";
+ } else if (status != 0x01) {
+ qWarning() << "Blob Command failed:" << status;
+ } else { // All is well
+ if (m_currentCommand->m_database == BlobDBIdApp && m_currentCommand->m_command == OperationInsert) {
+ QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat);
+ QUuid appUuid = QUuid::fromRfc4122(m_currentCommand->m_key);
+ s.setValue(appUuid.toString(), true);
+ emit appInserted(appUuid);
+ }
+ }
+
+ if (m_currentCommand && token == m_currentCommand->m_token) {
+ delete m_currentCommand;
+ m_currentCommand = nullptr;
+ sendNext();
+ }
+}
+
+void BlobDB::actionInvoked(const QByteArray &actionReply)
+{
+ WatchDataReader reader(actionReply);
+ TimelineAction::Type actionType = (TimelineAction::Type)reader.read<quint8>();
+ QUuid notificationId = reader.readUuid();
+ quint8 actionId = reader.read<quint8>();
+ quint8 param = reader.read<quint8>(); // Is this correct? So far I've only seen 0x00 in here
+
+ // Not sure what to do with those yet
+ Q_UNUSED(actionType)
+ Q_UNUSED(param)
+
+ qDebug() << "Action invoked" << actionId << actionReply.toHex();
+
+ Status status = StatusError;
+ QList<TimelineAttribute> attributes;
+
+ Notification notification = m_notificationSources.value(notificationId);
+ QString sourceId = notification.sourceId();
+ if (sourceId.isEmpty()) {
+ status = StatusError;
+ } else {
+ switch (actionId) {
+ case 1: { // Mute source
+ TimelineAttribute textAttribute(TimelineAttribute::TypeSubtitle, "Muted!");
+ attributes.append(textAttribute);
+// TimelineAttribute iconAttribute(TimelineAttribute::TypeLargeIcon, TimelineAttribute::IconIDTelegram);
+// attributes.append(iconAttribute);
+ emit muteSource(sourceId);
+ status = StatusSuccess;
+ break;
+ }
+ case 2: { // Open on phone
+ TimelineAttribute textAttribute(TimelineAttribute::TypeSubtitle, "Opened!");
+ attributes.append(textAttribute);
+ qDebug() << "opening" << notification.actToken();
+ emit actionTriggered(notification.actToken());
+ status = StatusSuccess;
+ }
+ }
+ }
+
+ QByteArray reply;
+ reply.append(0x11); // Length of id & status code
+ reply.append(notificationId.toRfc4122());
+ reply.append(status);
+ reply.append(attributes.count());
+ foreach (const TimelineAttribute &attrib, attributes) {
+ reply.append(attrib.serialize());
+ }
+ m_connection->writeToPebble(WatchConnection::EndpointActionHandler, reply);
+}
+
+void BlobDB::sendActionReply()
+{
+
+}
+
+void BlobDB::sendNext()
+{
+ if (m_currentCommand || m_commandQueue.isEmpty()) {
+ return;
+ }
+ m_currentCommand = m_commandQueue.takeFirst();
+ m_connection->writeToPebble(WatchConnection::EndpointBlobDB, m_currentCommand->serialize());
+}
+
+quint16 BlobDB::generateToken()
+{
+ return (qrand() % ((int)pow(2, 16) - 2)) + 1;
+}
+
+AppMetadata BlobDB::appInfoToMetadata(const AppInfo &info, HardwarePlatform hardwarePlatform)
+{
+ QString binaryFile = info.file(AppInfo::FileTypeApplication, hardwarePlatform);
+ QFile f(binaryFile);
+ if (!f.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening app binary";
+ return AppMetadata();
+ }
+ QByteArray data = f.read(512);
+ WatchDataReader reader(data);
+ qDebug() << "Header:" << reader.readFixedString(8);
+ qDebug() << "struct Major version:" << reader.read<quint8>();
+ qDebug() << "struct Minor version:" << reader.read<quint8>();
+ quint8 sdkVersionMajor = reader.read<quint8>();
+ qDebug() << "sdk Major version:" << sdkVersionMajor;
+ quint8 sdkVersionMinor = reader.read<quint8>();
+ qDebug() << "sdk Minor version:" << sdkVersionMinor;
+ quint8 appVersionMajor = reader.read<quint8>();
+ qDebug() << "app Major version:" << appVersionMajor;
+ quint8 appVersionMinor = reader.read<quint8>();
+ qDebug() << "app Minor version:" << appVersionMinor;
+ qDebug() << "size:" << reader.readLE<quint16>();
+ qDebug() << "offset:" << reader.readLE<quint32>();
+ qDebug() << "crc:" << reader.readLE<quint32>();
+ QString appName = reader.readFixedString(32);
+ qDebug() << "App name:" << appName;
+ qDebug() << "Vendor name:" << reader.readFixedString(32);
+ quint32 icon = reader.readLE<quint32>();
+ qDebug() << "Icon:" << icon;
+ qDebug() << "Symbol table address:" << reader.readLE<quint32>();
+ quint32 flags = reader.readLE<quint32>();
+ qDebug() << "Flags:" << flags;
+ qDebug() << "Num relocatable entries:" << reader.readLE<quint32>();
+
+ f.close();
+ qDebug() << "app data" << data.toHex();
+
+ AppMetadata metadata;
+ metadata.setUuid(info.uuid());
+ metadata.setFlags(flags);
+ metadata.setAppVersion(appVersionMajor, appVersionMinor);
+ metadata.setSDKVersion(sdkVersionMajor, sdkVersionMinor);
+ metadata.setAppFaceBgColor(0);
+ metadata.setAppFaceTemplateId(0);
+ metadata.setAppName(appName);
+ metadata.setIcon(icon);
+ return metadata;
+
+}
+
+CalendarEvent BlobDB::findCalendarEvent(const QString &id)
+{
+ foreach (const CalendarEvent &entry, m_calendarEntries) {
+ if (entry.id() == id) {
+ return entry;
+ }
+ }
+ return CalendarEvent();
+}
+
+QByteArray BlobDB::BlobCommand::serialize() const
+{
+ QByteArray ret;
+ ret.append((quint8)m_command);
+ ret.append(m_token & 0xFF); ret.append(((m_token >> 8) & 0xFF));
+ ret.append((quint8)m_database);
+
+ if (m_command == BlobDB::OperationInsert || m_command == BlobDB::OperationDelete) {
+ ret.append(m_key.length() & 0xFF);
+ ret.append(m_key);
+ }
+ if (m_command == BlobDB::OperationInsert) {
+ ret.append(m_value.length() & 0xFF); ret.append((m_value.length() >> 8) & 0xFF); // value length
+ ret.append(m_value);
+ }
+
+ return ret;
+}
diff --git a/rockworkd/libpebble/blobdb.h b/rockworkd/libpebble/blobdb.h
new file mode 100644
index 0000000..b1db403
--- /dev/null
+++ b/rockworkd/libpebble/blobdb.h
@@ -0,0 +1,108 @@
+#ifndef BLOBDB_H
+#define BLOBDB_H
+
+#include "watchconnection.h"
+#include "pebble.h"
+#include "timelineitem.h"
+#include "healthparams.h"
+#include "appmetadata.h"
+
+#include <QObject>
+#include <QDateTime>
+#include <QOrganizerEvent>
+
+QTORGANIZER_USE_NAMESPACE
+
+
+class BlobDB : public QObject
+{
+ Q_OBJECT
+public:
+ enum BlobDBId {
+ BlobDBIdTest = 0,
+ BlobDBIdPin = 1,
+ BlobDBIdApp = 2,
+ BlobDBIdReminder = 3,
+ BlobDBIdNotification = 4,
+ BlobDBIdAppSettings = 7
+
+ };
+ enum Operation {
+ OperationInsert = 0x01,
+ OperationDelete = 0x04,
+ OperationClear = 0x05
+ };
+
+ enum Status {
+ StatusSuccess = 0x00,
+ StatusError = 0x01
+ };
+
+
+ explicit BlobDB(Pebble *pebble, WatchConnection *connection);
+
+ void insertNotification(const Notification &notification);
+ void insertTimelinePin(const QUuid &uuid, TimelineItem::Layout layout, const QDateTime &startTime, const QDateTime &endTime, const QString &title, const QString &desctiption, const QMap<QString, QString> fields, bool recurring);
+ void removeTimelinePin(const QUuid &uuid);
+ void insertReminder();
+ void clearTimeline();
+ void syncCalendar(const QList<CalendarEvent> &events);
+
+ void clearApps();
+ void insertAppMetaData(const AppInfo &info);
+ void removeApp(const AppInfo &info);
+
+ void insert(BlobDBId database, const TimelineItem &item);
+ void remove(BlobDBId database, const QUuid &uuid);
+ void clear(BlobDBId database);
+
+ void setHealthParams(const HealthParams &healthParams);
+ void setUnits(bool imperial);
+
+private slots:
+ void blobCommandReply(const QByteArray &data);
+ void actionInvoked(const QByteArray &data);
+ void sendActionReply();
+ void sendNext();
+
+signals:
+ void muteSource(const QString &sourceId);
+ void actionTriggered(const QString &actToken);
+ void appInserted(const QUuid &uuid);
+
+private:
+ quint16 generateToken();
+ AppMetadata appInfoToMetadata(const AppInfo &info, HardwarePlatform hardwarePlatform);
+
+private:
+
+ class BlobCommand: public PebblePacket
+ {
+ public:
+ BlobDB::Operation m_command; // quint8
+ quint16 m_token;
+ BlobDB::BlobDBId m_database;
+
+ QByteArray m_key;
+ QByteArray m_value;
+
+ QByteArray serialize() const override;
+ };
+
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+
+ QHash<QUuid, Notification> m_notificationSources;
+
+ QList<CalendarEvent> m_calendarEntries;
+ CalendarEvent findCalendarEvent(const QString &id);
+
+ HealthParams m_healthParams;
+
+ BlobCommand *m_currentCommand = nullptr;
+ QList<BlobCommand*> m_commandQueue;
+
+ QString m_blobDBStoragePath;
+};
+
+#endif // BLOBDB_H
diff --git a/rockworkd/libpebble/bluez/bluez_adapter1.cpp b/rockworkd/libpebble/bluez/bluez_adapter1.cpp
new file mode 100644
index 0000000..a386af1
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_adapter1.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAdapter1 -p bluez_adapter1 -v org.bluez.Adapter1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+#include "bluez_adapter1.h"
+
+/*
+ * Implementation of interface class BluezAdapter1
+ */
+
+BluezAdapter1::BluezAdapter1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+BluezAdapter1::~BluezAdapter1()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/bluez_adapter1.h b/rockworkd/libpebble/bluez/bluez_adapter1.h
new file mode 100644
index 0000000..8690075
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_adapter1.h
@@ -0,0 +1,66 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAdapter1 -p bluez_adapter1 -v org.bluez.Adapter1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef BLUEZ_ADAPTER1_H_1442480417
+#define BLUEZ_ADAPTER1_H_1442480417
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.bluez.Adapter1
+ */
+class BluezAdapter1: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.bluez.Adapter1"; }
+
+public:
+ BluezAdapter1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~BluezAdapter1();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<> RemoveDevice(const QDBusObjectPath &device)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(device);
+ return asyncCallWithArgumentList(QStringLiteral("RemoveDevice"), argumentList);
+ }
+
+ inline QDBusPendingReply<> StartDiscovery()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("StartDiscovery"), argumentList);
+ }
+
+ inline QDBusPendingReply<> StopDiscovery()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("StopDiscovery"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+};
+
+namespace org {
+ namespace bluez {
+ typedef ::BluezAdapter1 Adapter1;
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp b/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp
new file mode 100644
index 0000000..630953b
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAgentManager1 -p bluez_agentmanager1 org.bluez.AgentManager1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+#include "bluez_agentmanager1.h"
+
+/*
+ * Implementation of interface class BluezAgentManager1
+ */
+
+BluezAgentManager1::BluezAgentManager1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+BluezAgentManager1::~BluezAgentManager1()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/bluez_agentmanager1.h b/rockworkd/libpebble/bluez/bluez_agentmanager1.h
new file mode 100644
index 0000000..5f50e0d
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_agentmanager1.h
@@ -0,0 +1,68 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezAgentManager1 -p bluez_agentmanager1 org.bluez.AgentManager1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef BLUEZ_AGENTMANAGER1_H_1442489332
+#define BLUEZ_AGENTMANAGER1_H_1442489332
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.bluez.AgentManager1
+ */
+class BluezAgentManager1: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.bluez.AgentManager1"; }
+
+public:
+ BluezAgentManager1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~BluezAgentManager1();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<> RegisterAgent(const QDBusObjectPath &agent, const QString &capability)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(agent) << QVariant::fromValue(capability);
+ return asyncCallWithArgumentList(QStringLiteral("RegisterAgent"), argumentList);
+ }
+
+ inline QDBusPendingReply<> RequestDefaultAgent(const QDBusObjectPath &agent)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(agent);
+ return asyncCallWithArgumentList(QStringLiteral("RequestDefaultAgent"), argumentList);
+ }
+
+ inline QDBusPendingReply<> UnregisterAgent(const QDBusObjectPath &agent)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(agent);
+ return asyncCallWithArgumentList(QStringLiteral("UnregisterAgent"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+};
+
+namespace org {
+ namespace bluez {
+ typedef ::BluezAgentManager1 AgentManager1;
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/bluez_device1.cpp b/rockworkd/libpebble/bluez/bluez_device1.cpp
new file mode 100644
index 0000000..b5ee0f8
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_device1.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezDevice1 -p bluez_device1 org.bluez.Device1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+#include "bluez_device1.h"
+
+/*
+ * Implementation of interface class BluezDevice1
+ */
+
+BluezDevice1::BluezDevice1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+BluezDevice1::~BluezDevice1()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/bluez_device1.h b/rockworkd/libpebble/bluez/bluez_device1.h
new file mode 100644
index 0000000..c9eaa1f
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_device1.h
@@ -0,0 +1,85 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c BluezDevice1 -p bluez_device1 org.bluez.Device1.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef BLUEZ_DEVICE1_H_1442480478
+#define BLUEZ_DEVICE1_H_1442480478
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.bluez.Device1
+ */
+class BluezDevice1: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.bluez.Device1"; }
+
+public:
+ BluezDevice1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~BluezDevice1();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<> CancelPairing()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("CancelPairing"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Connect()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("Connect"), argumentList);
+ }
+
+ inline QDBusPendingReply<> ConnectProfile(const QString &UUID)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(UUID);
+ return asyncCallWithArgumentList(QStringLiteral("ConnectProfile"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Disconnect()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("Disconnect"), argumentList);
+ }
+
+ inline QDBusPendingReply<> DisconnectProfile(const QString &UUID)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(UUID);
+ return asyncCallWithArgumentList(QStringLiteral("DisconnectProfile"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Pair()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("Pair"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+};
+
+namespace org {
+ namespace bluez {
+ typedef ::BluezDevice1 Device1;
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/bluez_helper.h b/rockworkd/libpebble/bluez/bluez_helper.h
new file mode 100644
index 0000000..363f7ae
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluez_helper.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+*/
+
+#ifndef BLUEZ_HELPER_H_
+#define BLUEZ_HELPER_H_
+
+#include <QObject>
+#include <QDBusObjectPath>
+
+typedef QMap<QString, QVariantMap> InterfaceList;
+typedef QMap<QDBusObjectPath, InterfaceList> ManagedObjectList;
+
+Q_DECLARE_METATYPE(InterfaceList)
+Q_DECLARE_METATYPE(ManagedObjectList)
+
+#endif
diff --git a/rockworkd/libpebble/bluez/bluezclient.cpp b/rockworkd/libpebble/bluez/bluezclient.cpp
new file mode 100644
index 0000000..8cdf848
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluezclient.cpp
@@ -0,0 +1,84 @@
+#include "bluezclient.h"
+#include "dbus-shared.h"
+
+#include <QDBusConnection>
+#include <QDBusReply>
+#include <QDebug>
+
+BluezClient::BluezClient(QObject *parent):
+ QObject(parent),
+ m_dbus(QDBusConnection::systemBus()),
+ m_bluezManager("org.bluez", "/", m_dbus),
+ m_bluezAgentManager("org.bluez", "/org/bluez", m_dbus)
+{
+ qDBusRegisterMetaType<InterfaceList>();
+ qDBusRegisterMetaType<ManagedObjectList>();
+
+ if (m_bluezManager.isValid()) {
+ connect(&m_bluezManager, SIGNAL(InterfacesAdded(const QDBusObjectPath&, InterfaceList)),
+ this, SLOT(slotInterfacesAdded(const QDBusObjectPath&, InterfaceList)));
+
+ connect(&m_bluezManager, SIGNAL(InterfacesRemoved(const QDBusObjectPath&, const QStringList&)),
+ this, SLOT(slotInterfacesRemoved(const QDBusObjectPath&, const QStringList&)));
+
+ auto objectList = m_bluezManager.GetManagedObjects().argumentAt<0>();
+ for (QDBusObjectPath path : objectList.keys()) {
+ InterfaceList ifaces = objectList.value(path);
+ if (ifaces.contains(BLUEZ_DEVICE_IFACE)) {
+ QString candidatePath = path.path();
+
+ auto properties = ifaces.value(BLUEZ_DEVICE_IFACE);
+ addDevice(path, properties);
+ }
+ }
+ }
+}
+
+QList<Device> BluezClient::pairedPebbles() const
+{
+ QList<Device> ret;
+ if (m_bluezManager.isValid()) {
+ foreach (const Device &dev, m_devices) {
+ ret << dev;
+ }
+ }
+ return ret;
+}
+
+void BluezClient::addDevice(const QDBusObjectPath &path, const QVariantMap &properties)
+{
+ QString address = properties.value("Address").toString();
+ QString name = properties.value("Name").toString();
+ if (name.startsWith("Pebble") && !name.startsWith("Pebble Time LE") && !name.startsWith("Pebble-LE") && !m_devices.contains(address)) {
+ qDebug() << "Found new Pebble:" << address << name;
+ Device device;
+ device.address = QBluetoothAddress(address);
+ device.name = name;
+ device.path = path.path();
+ m_devices.insert(path.path(), device);
+ qDebug() << "emitting added";
+ emit devicesChanged();
+ }
+}
+
+void BluezClient::slotInterfacesAdded(const QDBusObjectPath &path, InterfaceList ifaces)
+{
+ qDebug() << "Interface added!";
+ if (ifaces.contains(BLUEZ_DEVICE_IFACE)) {
+ auto properties = ifaces.value(BLUEZ_DEVICE_IFACE);
+ addDevice(path, properties);
+ }
+}
+
+void BluezClient::slotInterfacesRemoved(const QDBusObjectPath &path, const QStringList &ifaces)
+{
+ qDebug() << "interfaces removed" << path.path() << ifaces;
+ if (!ifaces.contains(BLUEZ_DEVICE_IFACE)) {
+ return;
+ }
+ if (m_devices.contains(path.path())) {
+ m_devices.take(path.path());
+ qDebug() << "removing dev";
+ emit devicesChanged();
+ }
+}
diff --git a/rockworkd/libpebble/bluez/bluezclient.h b/rockworkd/libpebble/bluez/bluezclient.h
new file mode 100644
index 0000000..f8e5749
--- /dev/null
+++ b/rockworkd/libpebble/bluez/bluezclient.h
@@ -0,0 +1,51 @@
+#ifndef BLUEZCLIENT_H
+#define BLUEZCLIENT_H
+
+#include <QList>
+#include <QBluetoothAddress>
+#include <QBluetoothLocalDevice>
+
+#include "bluez_helper.h"
+#include "freedesktop_objectmanager.h"
+#include "freedesktop_properties.h"
+#include "bluez_adapter1.h"
+#include "bluez_agentmanager1.h"
+
+class Device {
+public:
+ QBluetoothAddress address;
+ QString name;
+ QString path;
+};
+
+class BluezClient: public QObject
+{
+ Q_OBJECT
+
+public:
+ BluezClient(QObject *parent = 0);
+
+
+ QList<Device> pairedPebbles() const;
+
+private slots:
+ void addDevice(const QDBusObjectPath &path, const QVariantMap &properties);
+
+ void slotInterfacesAdded(const QDBusObjectPath&path, InterfaceList ifaces);
+ void slotInterfacesRemoved(const QDBusObjectPath&path, const QStringList &ifaces);
+
+signals:
+ void devicesChanged();
+
+private:
+ QDBusConnection m_dbus;
+ DBusObjectManagerInterface m_bluezManager;
+ BluezAgentManager1 m_bluezAgentManager;
+ BluezAdapter1 *m_bluezAdapter = nullptr;
+ FreeDesktopProperties *m_bluezAdapterProperties = nullptr;
+
+
+ QHash<QString, Device> m_devices;
+};
+
+#endif // BLUEZCLIENT_H
diff --git a/rockworkd/libpebble/bluez/dbus-shared.h b/rockworkd/libpebble/bluez/dbus-shared.h
new file mode 100644
index 0000000..01e9699
--- /dev/null
+++ b/rockworkd/libpebble/bluez/dbus-shared.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013-2015 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#ifndef USS_DBUS_SHARED_H
+#define USS_DBUS_SHARED_H
+
+#define DBUS_AGENT_PATH "/com/canonical/SettingsBluetoothAgent"
+#define DBUS_ADAPTER_AGENT_PATH "/com/canonical/SettingsBluetoothAgent/adapteragent"
+#define DBUS_AGENT_CAPABILITY "KeyboardDisplay"
+
+#define BLUEZ_SERVICE "org.bluez"
+
+#define BLUEZ_ADAPTER_IFACE "org.bluez.Adapter1"
+#define BLUEZ_DEVICE_IFACE "org.bluez.Device1"
+
+#define watchCall(call, func) \
+ QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); \
+ QObject::connect(watcher, &QDBusPendingCallWatcher::finished, func)
+
+#endif // USS_DBUS_SHARED_H
diff --git a/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp b/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp
new file mode 100644
index 0000000..71ca4ce
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -p freedesktop_objectmanager -i bluez_helper.h -v -c DBusObjectManagerInterface org.freedesktop.DBus.ObjectManager.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+#include "freedesktop_objectmanager.h"
+
+/*
+ * Implementation of interface class DBusObjectManagerInterface
+ */
+
+DBusObjectManagerInterface::DBusObjectManagerInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+DBusObjectManagerInterface::~DBusObjectManagerInterface()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/freedesktop_objectmanager.h b/rockworkd/libpebble/bluez/freedesktop_objectmanager.h
new file mode 100644
index 0000000..509c5fc
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_objectmanager.h
@@ -0,0 +1,58 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -p freedesktop_objectmanager -i bluez_helper.h -v -c DBusObjectManagerInterface org.freedesktop.DBus.ObjectManager.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef FREEDESKTOP_OBJECTMANAGER_H_1442473386
+#define FREEDESKTOP_OBJECTMANAGER_H_1442473386
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+#include "bluez_helper.h"
+
+/*
+ * Proxy class for interface org.freedesktop.DBus.ObjectManager
+ */
+class DBusObjectManagerInterface: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.freedesktop.DBus.ObjectManager"; }
+
+public:
+ DBusObjectManagerInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~DBusObjectManagerInterface();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<ManagedObjectList> GetManagedObjects()
+ {
+ QList<QVariant> argumentList;
+ return asyncCallWithArgumentList(QStringLiteral("GetManagedObjects"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+ void InterfacesAdded(const QDBusObjectPath &object_path, InterfaceList interfaces_and_properties);
+ void InterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces);
+};
+
+namespace org {
+ namespace freedesktop {
+ namespace DBus {
+ typedef ::DBusObjectManagerInterface ObjectManager;
+ }
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/freedesktop_properties.cpp b/rockworkd/libpebble/bluez/freedesktop_properties.cpp
new file mode 100644
index 0000000..c74347c
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_properties.cpp
@@ -0,0 +1,26 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c FreeDesktopProperties -p freedesktop_properties -v org.freedesktop.DBus.Properties.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * This file may have been hand-edited. Look for HAND-EDIT comments
+ * before re-generating it.
+ */
+
+#include "freedesktop_properties.h"
+
+/*
+ * Implementation of interface class FreeDesktopProperties
+ */
+
+FreeDesktopProperties::FreeDesktopProperties(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
+ : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
+{
+}
+
+FreeDesktopProperties::~FreeDesktopProperties()
+{
+}
+
diff --git a/rockworkd/libpebble/bluez/freedesktop_properties.h b/rockworkd/libpebble/bluez/freedesktop_properties.h
new file mode 100644
index 0000000..a7a655c
--- /dev/null
+++ b/rockworkd/libpebble/bluez/freedesktop_properties.h
@@ -0,0 +1,71 @@
+/*
+ * This file was generated by qdbusxml2cpp version 0.8
+ * Command line was: qdbusxml2cpp -c FreeDesktopProperties -p freedesktop_properties -v org.freedesktop.DBus.Properties.xml
+ *
+ * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
+ *
+ * This is an auto-generated file.
+ * Do not edit! All changes made to it will be lost.
+ */
+
+#ifndef FREEDESKTOP_PROPERTIES_H_1442473392
+#define FREEDESKTOP_PROPERTIES_H_1442473392
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QVariant>
+#include <QtDBus/QtDBus>
+
+/*
+ * Proxy class for interface org.freedesktop.DBus.Properties
+ */
+class FreeDesktopProperties: public QDBusAbstractInterface
+{
+ Q_OBJECT
+public:
+ static inline const char *staticInterfaceName()
+ { return "org.freedesktop.DBus.Properties"; }
+
+public:
+ FreeDesktopProperties(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0);
+
+ ~FreeDesktopProperties();
+
+public Q_SLOTS: // METHODS
+ inline QDBusPendingReply<QDBusVariant> Get(const QString &interface, const QString &name)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(interface) << QVariant::fromValue(name);
+ return asyncCallWithArgumentList(QStringLiteral("Get"), argumentList);
+ }
+
+ inline QDBusPendingReply<QVariantMap> GetAll(const QString &interface)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(interface);
+ return asyncCallWithArgumentList(QStringLiteral("GetAll"), argumentList);
+ }
+
+ inline QDBusPendingReply<> Set(const QString &interface, const QString &name, const QDBusVariant &value)
+ {
+ QList<QVariant> argumentList;
+ argumentList << QVariant::fromValue(interface) << QVariant::fromValue(name) << QVariant::fromValue(value);
+ return asyncCallWithArgumentList(QStringLiteral("Set"), argumentList);
+ }
+
+Q_SIGNALS: // SIGNALS
+ void PropertiesChanged(const QString &interface, const QVariantMap &changed_properties, const QStringList &invalidated_properties);
+};
+
+namespace org {
+ namespace freedesktop {
+ namespace DBus {
+ typedef ::FreeDesktopProperties Properties;
+ }
+ }
+}
+#endif
diff --git a/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml b/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml
new file mode 100644
index 0000000..e535c7e
--- /dev/null
+++ b/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.bluez.AgentManager1">
+ <method name="RegisterAgent">
+ <arg type="o" name="agent"/>
+ <arg type="s" name="capability"/>
+ </method>
+ <method name="UnregisterAgent">
+ <arg type="o" name="agent"/>
+ </method>
+ <method name="RequestDefaultAgent">
+ <arg type="o" name="agent"/>
+ </method>
+ </interface>
+</node>
diff --git a/rockworkd/libpebble/bundle.cpp b/rockworkd/libpebble/bundle.cpp
new file mode 100644
index 0000000..64061c8
--- /dev/null
+++ b/rockworkd/libpebble/bundle.cpp
@@ -0,0 +1,151 @@
+#include "bundle.h"
+
+#include <QVariantMap>
+#include <QFileInfo>
+#include <QDebug>
+#include <QJsonParseError>
+
+Bundle::Bundle(const QString &path):
+ m_path(path)
+{
+
+}
+
+QString Bundle::path() const
+{
+ return m_path;
+}
+
+QString Bundle::file(Bundle::FileType type, HardwarePlatform hardwarePlatform) const
+{
+ // Those two will always be in the top level dir. HardwarePlatform is irrelevant.
+ switch (type) {
+ case FileTypeAppInfo:
+ return m_path + "/appInfo.js";
+ case FileTypeJsApp:
+ return m_path + "/pebble-js-app.js";
+ default:
+ ;
+ }
+
+ // For all the others we have to find the manifest file
+ QList<QString> possibleDirs;
+
+ switch (hardwarePlatform) {
+ case HardwarePlatformAplite:
+ if (QFileInfo::exists(path() + "/aplite/")) {
+ possibleDirs.append("aplite");
+ }
+ possibleDirs.append("");
+ break;
+ case HardwarePlatformBasalt:
+ if (QFileInfo::exists(path() + "/basalt/")) {
+ possibleDirs.append("basalt");
+ }
+ possibleDirs.append("");
+ break;
+ case HardwarePlatformChalk:
+ if (QFileInfo::exists(path() + "/chalk/")) {
+ possibleDirs.append("chalk");
+ }
+ break;
+ default:
+ possibleDirs.append("");
+ ;
+ }
+
+ QString manifestFilename;
+ QString subDir;
+ foreach (const QString &dir, possibleDirs) {
+ if (QFileInfo::exists(m_path + "/" + dir + "/manifest.json")) {
+ subDir = "/" + dir;
+ manifestFilename = m_path + subDir + "/manifest.json";
+ break;
+ }
+ }
+ if (manifestFilename.isEmpty()) {
+ qWarning() << "Error finding manifest.json";
+ return QString();
+ }
+
+ // We want the manifiest file. just return it without parsing it
+ if (type == FileTypeManifest) {
+ return manifestFilename;
+ }
+
+ QFile manifest(manifestFilename);
+ if (!manifest.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening" << manifestFilename;
+ return QString();
+ }
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(manifest.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing" << manifestFilename;
+ return QString();
+ }
+
+ QVariantMap manifestMap = jsonDoc.toVariant().toMap();
+ switch (type) {
+ case FileTypeApplication:
+ return m_path + subDir + "/" + manifestMap.value("application").toMap().value("name").toString();
+ case FileTypeResources:
+ if (manifestMap.contains("resources")) {
+ return m_path + subDir + "/" + manifestMap.value("resources").toMap().value("name").toString();
+ }
+ break;
+ case FileTypeWorker:
+ if (manifestMap.contains("worker")) {
+ return m_path + subDir + "/" + manifestMap.value("worker").toMap().value("name").toString();
+ }
+ break;
+ case FileTypeFirmware:
+ if (manifestMap.contains("firmware")) {
+ return m_path + subDir + "/" + manifestMap.value("firmware").toMap().value("name").toString();
+ }
+ break;
+ default:
+ ;
+ }
+ return QString();
+}
+
+quint32 Bundle::crc(Bundle::FileType type, HardwarePlatform hardwarePlatform) const
+{
+ switch (type) {
+ case FileTypeAppInfo:
+ case FileTypeJsApp:
+ case FileTypeManifest:
+ qWarning() << "Cannot get crc for file type" << type;
+ return 0;
+ default: ;
+ }
+
+ QFile manifest(file(FileTypeManifest, hardwarePlatform));
+ if (!manifest.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening manifest file";
+ return 0;
+ }
+
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(manifest.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing manifest file";
+ return 0;
+ }
+
+ QVariantMap manifestMap = jsonDoc.toVariant().toMap();
+ switch (type) {
+ case FileTypeApplication:
+ return manifestMap.value("application").toMap().value("crc").toUInt();
+ case FileTypeResources:
+ return manifestMap.value("resources").toMap().value("crc").toUInt();
+ case FileTypeWorker:
+ return manifestMap.value("worker").toMap().value("crc").toUInt();
+ case FileTypeFirmware:
+ return manifestMap.value("firmware").toMap().value("crc").toUInt();
+ default:
+ ;
+ }
+ return 0;
+}
diff --git a/rockworkd/libpebble/bundle.h b/rockworkd/libpebble/bundle.h
new file mode 100644
index 0000000..5ff5a16
--- /dev/null
+++ b/rockworkd/libpebble/bundle.h
@@ -0,0 +1,33 @@
+#ifndef BUNDLE_H
+#define BUNDLE_H
+
+#include <QString>
+
+#include "enums.h"
+
+class Bundle
+{
+public:
+ enum FileType {
+ FileTypeAppInfo,
+ FileTypeJsApp,
+ FileTypeManifest,
+ FileTypeApplication,
+ FileTypeResources,
+ FileTypeWorker,
+ FileTypeFirmware
+ };
+
+ Bundle(const QString &path = QString());
+
+ QString path() const;
+
+ QString file(FileType type, HardwarePlatform hardwarePlatform = HardwarePlatformUnknown) const;
+ quint32 crc(FileType type, HardwarePlatform hardwarePlatform = HardwarePlatformUnknown) const;
+
+private:
+ QString m_path;
+
+};
+
+#endif // BUNDLE_H
diff --git a/rockworkd/libpebble/calendarevent.cpp b/rockworkd/libpebble/calendarevent.cpp
new file mode 100644
index 0000000..ea99b56
--- /dev/null
+++ b/rockworkd/libpebble/calendarevent.cpp
@@ -0,0 +1,184 @@
+#include "calendarevent.h"
+
+#include <QSettings>
+#include <QFile>
+#include <QTimeZone>
+
+CalendarEvent::CalendarEvent():
+ m_uuid(QUuid::createUuid())
+{
+}
+
+bool CalendarEvent::isValid() const
+{
+ return !m_id.isNull();
+}
+
+QString CalendarEvent::id() const
+{
+ return m_id;
+}
+
+void CalendarEvent::setId(const QString &id)
+{
+ m_id = id;
+}
+
+QUuid CalendarEvent::uuid() const
+{
+ return m_uuid;
+}
+
+void CalendarEvent::generateNewUuid()
+{
+ m_uuid = QUuid::createUuid();
+}
+
+QString CalendarEvent::title() const
+{
+ return m_title;
+}
+
+void CalendarEvent::setTitle(const QString &title)
+{
+ m_title = title;
+}
+
+QString CalendarEvent::description() const
+{
+ return m_description;
+}
+
+void CalendarEvent::setDescription(const QString &description)
+{
+ m_description = description;
+}
+
+QDateTime CalendarEvent::startTime() const
+{
+ return m_startTime;
+}
+
+void CalendarEvent::setStartTime(const QDateTime &startTime)
+{
+ m_startTime = startTime;
+}
+
+QDateTime CalendarEvent::endTime() const
+{
+ return m_endTime;
+}
+
+void CalendarEvent::setEndTime(const QDateTime &endTime)
+{
+ m_endTime = endTime;
+}
+
+QString CalendarEvent::location() const
+{
+ return m_location;
+}
+
+void CalendarEvent::setLocation(const QString &location)
+{
+ m_location = location;
+}
+
+QString CalendarEvent::calendar() const
+{
+ return m_calendar;
+}
+
+void CalendarEvent::setCalendar(const QString &calendar)
+{
+ m_calendar = calendar;
+}
+
+QString CalendarEvent::comment() const
+{
+ return m_comment;
+}
+
+void CalendarEvent::setComment(const QString &comment)
+{
+ m_comment = comment;
+}
+
+QStringList CalendarEvent::guests() const
+{
+ return m_guests;
+}
+
+void CalendarEvent::setGuests(const QStringList &guests)
+{
+ m_guests = guests;
+}
+
+bool CalendarEvent::recurring() const
+{
+ return m_recurring;
+}
+
+void CalendarEvent::setRecurring(bool recurring)
+{
+ m_recurring = recurring;
+}
+
+bool CalendarEvent::operator==(const CalendarEvent &other) const
+{
+ // Storing a QDateTime to QSettings seems to lose time zone information. Lets ignore the time zone when
+ // comparing or we'll never find ourselves again.
+ QDateTime thisStartTime = m_startTime;
+ thisStartTime.setTimeZone(other.startTime().timeZone());
+ QDateTime thisEndTime = m_endTime;
+ thisEndTime.setTimeZone(other.endTime().timeZone());
+ return m_id == other.id()
+ && m_title == other.title()
+ && m_description == other.description()
+ && thisStartTime == other.startTime()
+ && thisEndTime == other.endTime()
+ && m_location == other.location()
+ && m_calendar == other.calendar()
+ && m_comment == other.comment()
+ && m_guests == other.guests()
+ && m_recurring == other.recurring();
+
+}
+
+void CalendarEvent::saveToCache(const QString &cachePath) const
+{
+ QSettings s(cachePath + "/calendarevent-" + m_uuid.toString(), QSettings::IniFormat);
+ s.setValue("id", m_id);
+ s.setValue("uuid", m_uuid);
+ s.setValue("title", m_title);
+ s.setValue("description", m_description);
+ s.setValue("startTime", m_startTime);
+ s.setValue("endTime", m_endTime);
+ s.setValue("location", m_location);
+ s.setValue("calendar", m_calendar);
+ s.setValue("comment", m_comment);
+ s.setValue("guests", m_guests);
+ s.setValue("recurring", m_recurring);
+}
+
+void CalendarEvent::loadFromCache(const QString &cachePath, const QString &uuid)
+{
+ m_uuid = uuid;
+ QSettings s(cachePath + "/calendarevent-" + m_uuid.toString(), QSettings::IniFormat);
+ m_id = s.value("id").toString();
+ m_title = s.value("title").toString();
+ m_description = s.value("description").toString();
+ m_startTime = s.value("startTime").toDateTime();
+ m_endTime = s.value("endTime").toDateTime();
+ m_location = s.value("location").toString();
+ m_calendar = s.value("calendar").toString();
+ m_comment = s.value("comment").toString();
+ m_guests = s.value("guests").toStringList();
+ m_recurring = s.value("recurring").toBool();
+}
+
+void CalendarEvent::removeFromCache(const QString &cachePath) const
+{
+ QFile::remove(cachePath + "/calendarevent-" + m_uuid.toString());
+}
+
diff --git a/rockworkd/libpebble/calendarevent.h b/rockworkd/libpebble/calendarevent.h
new file mode 100644
index 0000000..5361a48
--- /dev/null
+++ b/rockworkd/libpebble/calendarevent.h
@@ -0,0 +1,69 @@
+#ifndef CALENDAREVENT_H
+#define CALENDAREVENT_H
+
+#include <QString>
+#include <QStringList>
+#include <QDateTime>
+#include <QUuid>
+
+class CalendarEvent
+{
+public:
+ CalendarEvent();
+
+ bool isValid() const;
+
+ QString id() const;
+ void setId(const QString &id);
+
+ QUuid uuid() const;
+ void generateNewUuid();
+
+ QString title() const;
+ void setTitle(const QString &title);
+
+ QString description() const;
+ void setDescription(const QString &description);
+
+ QDateTime startTime() const;
+ void setStartTime(const QDateTime &startTime);
+
+ QDateTime endTime() const;
+ void setEndTime(const QDateTime &endTime);
+
+ QString location() const;
+ void setLocation(const QString &location);
+
+ QString calendar() const;
+ void setCalendar(const QString &calendar);
+
+ QString comment() const;
+ void setComment(const QString &comment);
+
+ QStringList guests() const;
+ void setGuests(const QStringList &guests);
+
+ bool recurring() const;
+ void setRecurring(bool recurring);
+
+ bool operator==(const CalendarEvent &other) const;
+
+ void saveToCache(const QString &cachePath) const;
+ void loadFromCache(const QString &cachePath, const QString &uuid);
+ void removeFromCache(const QString &cachePath) const;
+
+private:
+ QString m_id;
+ QUuid m_uuid;
+ QString m_title;
+ QString m_description;
+ QDateTime m_startTime;
+ QDateTime m_endTime;
+ QString m_location;
+ QString m_calendar;
+ QString m_comment;
+ QStringList m_guests;
+ bool m_recurring = false;
+};
+
+#endif // CALENDAREVENT_H
diff --git a/rockworkd/libpebble/dataloggingendpoint.cpp b/rockworkd/libpebble/dataloggingendpoint.cpp
new file mode 100644
index 0000000..a571c25
--- /dev/null
+++ b/rockworkd/libpebble/dataloggingendpoint.cpp
@@ -0,0 +1,44 @@
+#include "dataloggingendpoint.h"
+
+#include "pebble.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+DataLoggingEndpoint::DataLoggingEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointDataLogging, this, "handleMessage");
+}
+
+void DataLoggingEndpoint::handleMessage(const QByteArray &data)
+{
+ qDebug() << "data logged" << data.toHex();
+ WatchDataReader reader(data);
+ DataLoggingCommand command = (DataLoggingCommand)reader.read<quint8>();
+ switch (command) {
+ case DataLoggingDespoolSendData: {
+ quint8 sessionId = reader.read<quint8>();
+ quint32 itemsLeft = reader.readLE<quint32>();
+ quint32 crc = reader.readLE<quint32>();
+ qDebug() << "Despooling data: Session:" << sessionId << "Items left:" << itemsLeft << "CRC:" << crc;
+
+ QByteArray reply;
+ WatchDataWriter writer(&reply);
+ writer.write<quint8>(DataLoggingACK);
+ writer.write<quint8>(sessionId);
+ m_connection->writeToPebble(WatchConnection::EndpointDataLogging, reply);
+ return;
+ }
+ case DataLoggingTimeout: {
+ quint8 sessionId = reader.read<quint8>();
+ qDebug() << "DataLogging reached timeout: Session:" << sessionId;
+ return;
+ }
+ default:
+ qDebug() << "Unhandled DataLogging message";
+ }
+}
+
diff --git a/rockworkd/libpebble/dataloggingendpoint.h b/rockworkd/libpebble/dataloggingendpoint.h
new file mode 100644
index 0000000..2c5dfc5
--- /dev/null
+++ b/rockworkd/libpebble/dataloggingendpoint.h
@@ -0,0 +1,39 @@
+#ifndef DATALOGGINGENDPOINT_H
+#define DATALOGGINGENDPOINT_H
+
+#include <QObject>
+
+class Pebble;
+class WatchConnection;
+
+class DataLoggingEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum DataLoggingCommand {
+ DataLoggingDespoolOpenSession = 0x01,
+ DataLoggingDespoolSendData = 0x02,
+ DataLoggingCloseSession = 0x03,
+ DataLoggingReportOpenSessions = 0x84,
+ DataLoggingACK = 0x85,
+ DataLoggingNACK = 0x86,
+ DataLoggingTimeout = 0x07,
+ DataLoggingEmptySession = 0x88,
+ DataLoggingGetSendEnableRequest = 0x89,
+ DataLoggingGetSendEnableResponse = 0x0A,
+ DataLoggingSetSendEnable = 0x8B
+ };
+
+ explicit DataLoggingEndpoint(Pebble *pebble, WatchConnection *connection);
+
+signals:
+
+private slots:
+ void handleMessage(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+};
+
+#endif // DATALOGGINGENDPOINT_H
diff --git a/rockworkd/libpebble/enums.h b/rockworkd/libpebble/enums.h
new file mode 100644
index 0000000..d6184c6
--- /dev/null
+++ b/rockworkd/libpebble/enums.h
@@ -0,0 +1,95 @@
+#ifndef ENUMS_H
+#define ENUMS_H
+
+#include <QMetaType>
+
+enum HardwareRevision {
+ HardwareRevisionUNKNOWN = 0,
+ HardwareRevisionTINTIN_EV1 = 1,
+ HardwareRevisionTINTIN_EV2 = 2,
+ HardwareRevisionTINTIN_EV2_3 = 3,
+ HardwareRevisionTINTIN_EV2_4 = 4,
+ HardwareRevisionTINTIN_V1_5 = 5,
+ HardwareRevisionBIANCA = 6,
+ HardwareRevisionSNOWY_EVT2 = 7,
+ HardwareRevisionSNOWY_DVT = 8,
+ HardwareRevisionSPALDING_EVT = 9,
+ HardwareRevisionBOBBY_SMILES = 10,
+ HardwareRevisionSPALDING = 11,
+
+ HardwareRevisionTINTIN_BB = 0xFF,
+ HardwareRevisionTINTIN_BB2 = 0xFE,
+ HardwareRevisionSNOWY_BB = 0xFD,
+ HardwareRevisionSNOWY_BB2 = 0xFC,
+ HardwareRevisionSPALDING_BB2 = 0xFB
+};
+
+enum OS {
+ OSUnknown = 0,
+ OSiOS = 1,
+ OSAndroid = 2,
+ OSOSX = 3,
+ OSLinux = 4,
+ OSWindows = 5
+};
+
+enum HardwarePlatform {
+ HardwarePlatformUnknown = 0,
+ HardwarePlatformAplite,
+ HardwarePlatformBasalt,
+ HardwarePlatformChalk
+};
+
+enum Model {
+ ModelUnknown = 0,
+ ModelTintinBlack = 1,
+ ModelTintinWhite = 2,
+ ModelTintinRed = 3,
+ ModelTintinOrange = 4,
+ ModelTintinGrey = 5,
+ ModelBiancaSilver = 6,
+ ModelBiancaBlack = 7,
+ ModelTintinBlue = 8,
+ ModelTintinGreen = 9,
+ ModelTintinPink = 10,
+ ModelSnowyWhite = 11,
+ ModelSnowyBlack = 12,
+ ModelSnowyRed = 13,
+ ModelBobbySilver = 14,
+ ModelBobbyBlack = 15,
+ ModelBobbyGold = 16,
+ ModelSpalding14Silver = 17,
+ ModelSpalding14Black = 18,
+ ModelSpalding20Silver = 19,
+ ModelSpalding20Black = 20,
+ ModelSpalding14RoseGold = 21
+};
+
+enum MusicControlButton {
+ MusicControlPlayPause,
+ MusicControlSkipBack,
+ MusicControlSkipNext,
+ MusicControlVolumeUp,
+ MusicControlVolumeDown
+};
+
+enum CallStatus {
+ CallStatusIncoming,
+ CallStatusOutGoing
+};
+
+enum Capability {
+ CapabilityNone = 0x0000000000000000,
+ CapabilityAppRunState = 0x0000000000000001,
+ CapabilityInfiniteLogDumping = 0x0000000000000002,
+ CapabilityUpdatedMusicProtocol = 0x0000000000000004,
+ CapabilityExtendedNotifications = 0x0000000000000008,
+ CapabilityLanguagePacks = 0x0000000000000010,
+ Capability8kAppMessages = 0x0000000000000020,
+ CapabilityHealth = 0x0000000000000040,
+ CapabilityVoice = 0x0000000000000080
+};
+Q_DECLARE_FLAGS(Capabilities, Capability)
+
+#endif // ENUMS_H
+
diff --git a/rockworkd/libpebble/firmwaredownloader.cpp b/rockworkd/libpebble/firmwaredownloader.cpp
new file mode 100644
index 0000000..5d32f3b
--- /dev/null
+++ b/rockworkd/libpebble/firmwaredownloader.cpp
@@ -0,0 +1,246 @@
+#include "firmwaredownloader.h"
+#include "ziphelper.h"
+#include "pebble.h"
+#include "watchconnection.h"
+#include "uploadmanager.h"
+
+#include <QNetworkAccessManager>
+#include <QUrlQuery>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QJsonDocument>
+#include <QFile>
+#include <QDir>
+#include <QCryptographicHash>
+
+FirmwareDownloader::FirmwareDownloader(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_nam = new QNetworkAccessManager(this);
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointSystemMessage, this, "systemMessageReceived");
+}
+
+bool FirmwareDownloader::updateAvailable() const
+{
+ return m_updateAvailable;
+}
+
+QString FirmwareDownloader::candidateVersion() const
+{
+ return m_candidateVersion;
+}
+
+QString FirmwareDownloader::releaseNotes() const
+{
+ return m_releaseNotes;
+}
+
+QString FirmwareDownloader::url() const
+{
+ return m_url;
+}
+
+bool FirmwareDownloader::upgrading() const
+{
+ return m_upgradeInProgress;
+}
+
+void FirmwareDownloader::performUpgrade()
+{
+ if (!m_updateAvailable) {
+ qWarning() << "No update available";
+ return;
+ }
+
+ if (m_upgradeInProgress) {
+ qWarning() << "Upgrade already in progress. Won't start another one";
+ return;
+ }
+
+ m_upgradeInProgress = true;
+ emit upgradingChanged();
+
+ QNetworkRequest request(m_url);
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply](){
+ reply->deleteLater();
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qWarning() << "Erorr fetching firmware" << reply->errorString();
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ QByteArray data = reply->readAll();
+
+ QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex();
+
+ if (hash != m_hash) {
+ qWarning() << "Downloaded data hash doesn't match hash from target";
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ QDir dir("/tmp/" + m_pebble->address().toString().replace(":", "_"));
+ if (!dir.exists() && !dir.mkpath(dir.absolutePath())) {
+ qWarning() << "Error saving file" << dir.absolutePath();
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+ QString path = "/tmp/" + m_pebble->address().toString().replace(":", "_");
+ QFile f(path + "/" + reply->request().url().fileName());
+ if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Cannot open tmp file for writing" << f.fileName();
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+ f.write(data);
+ f.close();
+
+ if (!ZipHelper::unpackArchive(f.fileName(), path)) {
+ qWarning() << "Error unpacking firmware archive";
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ Bundle firmware(path);
+ if (firmware.file(Bundle::FileTypeFirmware).isEmpty() || firmware.file(Bundle::FileTypeResources).isEmpty()) {
+ qWarning() << "Firmware bundle file missing binary or resources";
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ return;
+ }
+
+ qDebug() << "** Starting firmware upgrade **";
+ m_bundlePath = path;
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareStart);
+
+ });
+}
+
+void FirmwareDownloader::checkForNewFirmware()
+{
+ QString platformString;
+ switch (m_pebble->hardwareRevision()) {
+ case HardwareRevisionUNKNOWN:
+ case HardwareRevisionTINTIN_EV1:
+ case HardwareRevisionTINTIN_EV2:
+ case HardwareRevisionTINTIN_EV2_3:
+ case HardwareRevisionSNOWY_EVT2:
+ case HardwareRevisionSPALDING_EVT:
+ case HardwareRevisionTINTIN_BB:
+ case HardwareRevisionTINTIN_BB2:
+ case HardwareRevisionSNOWY_BB:
+ case HardwareRevisionSNOWY_BB2:
+ case HardwareRevisionSPALDING_BB2:
+ qWarning() << "Hardware revision not supported for firmware upgrades" << m_pebble->hardwareRevision();
+ return;
+ case HardwareRevisionTINTIN_EV2_4:
+ platformString = "ev2_4";
+ break;
+ case HardwareRevisionTINTIN_V1_5:
+ platformString = "v1_5";
+ break;
+ case HardwareRevisionBIANCA:
+ platformString = "v2_0";
+ break;
+ case HardwareRevisionSNOWY_DVT:
+ platformString = "snowy_dvt";
+ break;
+ case HardwareRevisionBOBBY_SMILES:
+ platformString = "snowy_s3";
+ break;
+ case HardwareRevisionSPALDING:
+ platformString = "spalding";
+ break;
+
+ }
+
+ QString url("https://pebblefw.s3.amazonaws.com/pebble/%1/%2/latest.json");
+ url = url.arg(platformString).arg("release-v3.8");
+ QNetworkRequest request(url);
+ QNetworkReply *reply = m_nam->get(request);
+ connect(reply, &QNetworkReply::finished, [this, reply]() {
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Error parsing firmware fetch reply" << jsonDoc.toJson(QJsonDocument::Indented);
+ return;
+ }
+ QVariantMap resultMap = jsonDoc.toVariant().toMap();
+ if (!resultMap.contains("normal")) {
+ qWarning() << "Could not find normal firmware package" << jsonDoc.toJson(QJsonDocument::Indented);
+ return;
+ }
+
+
+ QVariantMap targetFirmware;
+ if (resultMap.contains("3.x-migration") && m_pebble->softwareVersion() < "v3.0.0") {
+ targetFirmware = resultMap.value("3.x-migration").toMap();
+ } else if (m_pebble->softwareVersion() >= "v3.0.0" &&
+ resultMap.value("normal").toMap().value("friendlyVersion").toString() > m_pebble->softwareVersion()){
+ targetFirmware = resultMap.value("normal").toMap();
+ }
+
+ if (targetFirmware.isEmpty()) {
+ qDebug() << "Watch firmware is up to date";
+ m_updateAvailable = false;
+ emit updateAvailableChanged();
+ return;
+ }
+
+ qDebug() << targetFirmware;
+
+ m_candidateVersion = targetFirmware.value("friendlyVersion").toString();
+ m_releaseNotes = targetFirmware.value("notes").toString();
+ m_url = targetFirmware.value("url").toString();
+ m_hash = targetFirmware.value("sha-256").toByteArray();
+ m_updateAvailable = true;
+ qDebug() << "candidate firmware upgrade" << m_candidateVersion << m_releaseNotes << m_url;
+ emit updateAvailableChanged();
+ });
+}
+
+void FirmwareDownloader::systemMessageReceived(const QByteArray &data)
+{
+ qDebug() << "system message" << data.toHex();
+
+ if (!m_upgradeInProgress) {
+ return;
+ }
+
+ Bundle firmware(m_bundlePath);
+
+ qDebug() << "** Uploading firmware resources...";
+ m_connection->uploadManager()->uploadFirmwareResources(firmware.file(Bundle::FileTypeResources), firmware.crc(Bundle::FileTypeResources), [this, firmware]() {
+ qDebug() << "** Firmware resources uploaded. OK";
+
+ qDebug() << "** Uploading firmware binary...";
+ m_connection->uploadManager()->uploadFirmwareBinary(false, firmware.file(Bundle::FileTypeFirmware), firmware.crc(Bundle::FileTypeFirmware), [this]() {
+ qDebug() << "** Firmware binary uploaded. OK";
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareComplete);
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ }, [this](int code) {
+ qWarning() << "** ERROR uploading firmware binary" << code;
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareFail);
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ });
+ },
+ [this](int code) {
+ qWarning() << "** ERROR uploading firmware resources" << code;
+ m_connection->systemMessage(WatchConnection::SystemMessageFirmwareFail);
+ m_upgradeInProgress = false;
+ emit upgradingChanged();
+ });
+}
+
diff --git a/rockworkd/libpebble/firmwaredownloader.h b/rockworkd/libpebble/firmwaredownloader.h
new file mode 100644
index 0000000..d7bd5b8
--- /dev/null
+++ b/rockworkd/libpebble/firmwaredownloader.h
@@ -0,0 +1,50 @@
+#ifndef FIRWAREDOWNLOADER_H
+#define FIRWAREDOWNLOADER_H
+
+#include <QObject>
+
+#include "watchconnection.h"
+
+class Pebble;
+class QNetworkAccessManager;
+
+class FirmwareDownloader : public QObject
+{
+ Q_OBJECT
+public:
+ explicit FirmwareDownloader(Pebble *pebble, WatchConnection *connection);
+
+ bool updateAvailable() const;
+ QString candidateVersion() const;
+ QString releaseNotes() const;
+ QString url() const;
+
+ bool upgrading() const;
+
+public slots:
+ void checkForNewFirmware();
+ void performUpgrade();
+
+signals:
+ void updateAvailableChanged();
+ void upgradingChanged();
+
+private slots:
+ void systemMessageReceived(const QByteArray &data);
+
+private:
+ QNetworkAccessManager *m_nam;
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+
+ bool m_updateAvailable = false;
+ QString m_candidateVersion;
+ QString m_releaseNotes;
+ QString m_url;
+ QByteArray m_hash;
+
+ bool m_upgradeInProgress = false;
+ QString m_bundlePath;
+};
+
+#endif // FIRWAREDOWNLOADER_H
diff --git a/rockworkd/libpebble/healthparams.cpp b/rockworkd/libpebble/healthparams.cpp
new file mode 100644
index 0000000..270d950
--- /dev/null
+++ b/rockworkd/libpebble/healthparams.cpp
@@ -0,0 +1,93 @@
+#include "healthparams.h"
+
+#include "watchdatawriter.h"
+
+HealthParams::HealthParams()
+{
+
+}
+
+bool HealthParams::enabled() const
+{
+ return m_enabled;
+}
+
+void HealthParams::setEnabled(bool enabled)
+{
+ m_enabled = enabled;
+}
+
+int HealthParams::height() const
+{
+ return m_height;
+}
+
+void HealthParams::setHeight(int height)
+{
+ m_height = height;
+}
+
+int HealthParams::weight() const
+{
+ return m_weight;
+}
+
+void HealthParams::setWeight(int weight)
+{
+ m_weight = weight;
+}
+
+bool HealthParams::moreActive() const
+{
+ return m_moreActive;
+}
+
+void HealthParams::setMoreActive(bool moreActive)
+{
+ m_moreActive = moreActive;
+}
+
+bool HealthParams::sleepMore() const
+{
+ return m_sleepMore;
+}
+
+void HealthParams::setSleepMore(bool sleepMore)
+{
+ m_sleepMore = sleepMore;
+}
+
+int HealthParams::age() const
+{
+ return m_age;
+}
+
+void HealthParams::setAge(int age)
+{
+ m_age = age;
+}
+
+HealthParams::Gender HealthParams::gender() const
+{
+ return m_gender;
+}
+
+void HealthParams::setGender(HealthParams::Gender gender)
+{
+ m_gender = gender;
+}
+
+QByteArray HealthParams::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.writeLE<quint16>(m_height * 10);
+ writer.writeLE<quint16>(m_weight * 100);
+ writer.write<quint8>(m_enabled ? 0x01 : 0x00);
+ writer.write<quint8>(m_moreActive ? 0x01 : 0x00);
+ writer.write<quint8>(m_sleepMore ? 0x01 : 0x00);
+ writer.write<quint8>(m_age);
+ writer.write<quint8>(m_gender);
+ return ret;
+}
+
diff --git a/rockworkd/libpebble/healthparams.h b/rockworkd/libpebble/healthparams.h
new file mode 100644
index 0000000..03dbfc1
--- /dev/null
+++ b/rockworkd/libpebble/healthparams.h
@@ -0,0 +1,52 @@
+#ifndef HEALTHPARAMS_H
+#define HEALTHPARAMS_H
+
+#include "watchconnection.h"
+
+class HealthParams: public PebblePacket
+{
+public:
+ enum Gender {
+ GenderFemale = 0x00,
+ GenderMale = 0x01
+ };
+
+ HealthParams();
+
+ bool enabled() const;
+ void setEnabled(bool enabled);
+
+ // In cm
+ int height() const;
+ void setHeight(int height);
+
+ // In kg
+ int weight() const;
+ void setWeight(int weight);
+
+ bool moreActive() const;
+ void setMoreActive(bool moreActive);
+
+ bool sleepMore() const;
+ void setSleepMore(bool sleepMore);
+
+ int age() const;
+ void setAge(int age);
+
+ Gender gender() const;
+ void setGender(Gender gender);
+
+ QByteArray serialize() const;
+
+private:
+ bool m_enabled = false;
+ int m_height = 0;
+ int m_weight = 0;
+ bool m_moreActive = false;
+ bool m_sleepMore = false;
+ int m_age = 0;
+ Gender m_gender = Gender::GenderFemale;
+
+};
+
+#endif // HEALTHPARAMS_H
diff --git a/rockworkd/libpebble/jskit/cacheLocalStorage.js b/rockworkd/libpebble/jskit/cacheLocalStorage.js
new file mode 100644
index 0000000..22588a9
--- /dev/null
+++ b/rockworkd/libpebble/jskit/cacheLocalStorage.js
@@ -0,0 +1,11 @@
+//Since we don't have JS 6 support, this hack will allow us to save changes to localStorage when using dot or square bracket notation
+
+for (var key in localStorage) {
+ _jskit.localstorage.setItem(key, localStorage.getItem(key));
+}
+
+for (var key in _jskit.localstorage.keys()) {
+ if (localStorage[key] === undefined) {
+ _jskit.localstorage.removeItem(key);
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jsfiles.qrc b/rockworkd/libpebble/jskit/jsfiles.qrc
new file mode 100644
index 0000000..4dfbc1d
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jsfiles.qrc
@@ -0,0 +1,7 @@
+<RCC>
+ <qresource prefix="/">
+ <file>typedarray.js</file>
+ <file>jskitsetup.js</file>
+ <file>cacheLocalStorage.js</file>
+ </qresource>
+</RCC>
diff --git a/rockworkd/libpebble/jskit/jskitconsole.cpp b/rockworkd/libpebble/jskit/jskitconsole.cpp
new file mode 100644
index 0000000..3d6c85c
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitconsole.cpp
@@ -0,0 +1,29 @@
+#include <QDebug>
+
+#include "jskitconsole.h"
+
+JSKitConsole::JSKitConsole(QObject *parent) :
+ QObject(parent),
+ l(metaObject()->className())
+{
+}
+
+void JSKitConsole::log(const QString &msg)
+{
+ qCDebug(l) << msg;
+}
+
+void JSKitConsole::warn(const QString &msg)
+{
+ qCWarning(l) << msg;
+}
+
+void JSKitConsole::error(const QString &msg)
+{
+ qCCritical(l) << msg;
+}
+
+void JSKitConsole::info(const QString &msg)
+{
+ qCDebug(l) << msg;
+}
diff --git a/rockworkd/libpebble/jskit/jskitconsole.h b/rockworkd/libpebble/jskit/jskitconsole.h
new file mode 100644
index 0000000..3896ae3
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitconsole.h
@@ -0,0 +1,20 @@
+#ifndef JSKITCONSOLE_H
+#define JSKITCONSOLE_H
+
+#include <QLoggingCategory>
+
+class JSKitConsole : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitConsole(QObject *parent=0);
+
+ Q_INVOKABLE void log(const QString &msg);
+ Q_INVOKABLE void warn(const QString &msg);
+ Q_INVOKABLE void error(const QString &msg);
+ Q_INVOKABLE void info(const QString &msg);
+};
+
+#endif // JSKITCONSOLE_H
diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.cpp b/rockworkd/libpebble/jskit/jskitgeolocation.cpp
new file mode 100644
index 0000000..409cda1
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitgeolocation.cpp
@@ -0,0 +1,302 @@
+#include <limits>
+
+#include "jskitgeolocation.h"
+
+JSKitGeolocation::JSKitGeolocation(QJSEngine *engine) :
+ QObject(engine),
+ l(metaObject()->className()),
+ m_engine(engine),
+ m_source(0),
+ m_lastWatcherId(0)
+{
+}
+
+void JSKitGeolocation::getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options)
+{
+ setupWatcher(successCallback, errorCallback, options, true);
+}
+
+int JSKitGeolocation::watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options)
+{
+ return setupWatcher(successCallback, errorCallback, options, false);
+}
+
+void JSKitGeolocation::clearWatch(int watcherId)
+{
+ removeWatcher(watcherId);
+}
+
+void JSKitGeolocation::handleError(QGeoPositionInfoSource::Error error)
+{
+ qCWarning(l) << "positioning error: " << error;
+
+ if (m_watchers.empty()) {
+ qCWarning(l) << "got position error but no one is watching";
+ stopAndRemove();
+ }
+ else {
+ QJSValue obj;
+ if (error == QGeoPositionInfoSource::AccessError) {
+ obj = buildPositionErrorObject(PERMISSION_DENIED, "permission denied");
+ } else {
+ obj = buildPositionErrorObject(POSITION_UNAVAILABLE, "position unavailable");
+ }
+
+ for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) {
+ invokeCallback(it->errorCallback, obj);
+
+ if (it->once) {
+ it = m_watchers.erase(it);
+ } else {
+ it->timer.restart();
+ ++it;
+ }
+ }
+ }
+}
+
+void JSKitGeolocation::handlePosition(const QGeoPositionInfo &pos)
+{
+ qCDebug(l) << "got position at" << pos.timestamp() << "type" << pos.coordinate().type();
+
+ if (m_watchers.empty()) {
+ qCWarning(l) << "got position update but no one is watching";
+ stopAndRemove();
+ }
+ else {
+ QJSValue obj = buildPositionObject(pos);
+
+ for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) {
+ invokeCallback(it->successCallback, obj);
+
+ if (it->once) {
+ it = m_watchers.erase(it);
+ } else {
+ it->timer.restart();
+ ++it;
+ }
+ }
+ }
+}
+
+void JSKitGeolocation::handleTimeout()
+{
+ qCDebug(l) << "positioning timeout";
+
+ if (m_watchers.empty()) {
+ qCWarning(l) << "got position timeout but no one is watching";
+ stopAndRemove();
+ }
+ else {
+ QJSValue obj = buildPositionErrorObject(TIMEOUT, "timeout");
+
+ for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) {
+ if (it->timer.hasExpired(it->timeout)) {
+ qCDebug(l) << "positioning timeout for watch" << it->watcherId
+ << ", watch is" << it->timer.elapsed() << "ms old, timeout is" << it->timeout;
+ invokeCallback(it->errorCallback, obj);
+
+ if (it->once) {
+ it = m_watchers.erase(it);
+ } else {
+ it->timer.restart();
+ ++it;
+ }
+ } else {
+ ++it;
+ }
+ }
+
+ QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection);
+ }
+}
+
+void JSKitGeolocation::updateTimeouts()
+{
+ int once_timeout = -1, updates_timeout = -1;
+
+ Q_FOREACH(const Watcher &watcher, m_watchers) {
+ qint64 rem_timeout = watcher.timeout - watcher.timer.elapsed();
+ qCDebug(l) << "watch" << watcher.watcherId << "rem timeout" << rem_timeout;
+
+ if (rem_timeout >= 0) {
+ // Make sure the limits aren't too large
+ rem_timeout = qMin<qint64>(rem_timeout, std::numeric_limits<int>::max());
+
+ if (watcher.once) {
+ once_timeout = once_timeout >= 0 ? qMin<int>(once_timeout, rem_timeout) : rem_timeout;
+ } else {
+ updates_timeout = updates_timeout >= 0 ? qMin<int>(updates_timeout, rem_timeout) : rem_timeout;
+ }
+ }
+ }
+
+ if (updates_timeout >= 0) {
+ qCDebug(l) << "setting location update interval to" << updates_timeout;
+ m_source->setUpdateInterval(updates_timeout);
+ m_source->startUpdates();
+ } else {
+ qCDebug(l) << "stopping updates";
+ m_source->stopUpdates();
+ }
+
+ if (once_timeout >= 0) {
+ qCDebug(l) << "requesting single location update with timeout" << once_timeout;
+ m_source->requestUpdate(once_timeout);
+ }
+
+ if (once_timeout == 0 && updates_timeout == 0) {
+ stopAndRemove();
+ }
+}
+
+int JSKitGeolocation::setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once)
+{
+ Watcher watcher;
+ watcher.successCallback = successCallback;
+ watcher.errorCallback = errorCallback;
+ watcher.highAccuracy = options.value("enableHighAccuracy", false).toBool();
+ watcher.timeout = options.value("timeout", std::numeric_limits<int>::max() - 1).toInt();
+ watcher.maximumAge = options.value("maximumAge", 0).toLongLong();
+ watcher.once = once;
+ watcher.watcherId = ++m_lastWatcherId;
+
+ qCDebug(l) << "setting up watcher, gps=" << watcher.highAccuracy << "timeout=" << watcher.timeout << "maximumAge=" << watcher.maximumAge << "once=" << watcher.once;
+
+ if (!m_source) {
+ m_source = QGeoPositionInfoSource::createDefaultSource(this);
+
+ connect(m_source, static_cast<void (QGeoPositionInfoSource::*)(QGeoPositionInfoSource::Error)>(&QGeoPositionInfoSource::error),
+ this, &JSKitGeolocation::handleError);
+ connect(m_source, &QGeoPositionInfoSource::positionUpdated,
+ this, &JSKitGeolocation::handlePosition);
+ connect(m_source, &QGeoPositionInfoSource::updateTimeout,
+ this, &JSKitGeolocation::handleTimeout);
+ }
+
+ if (watcher.maximumAge > 0) {
+ QDateTime threshold = QDateTime::currentDateTime().addMSecs(-qint64(watcher.maximumAge));
+ QGeoPositionInfo pos = m_source->lastKnownPosition(watcher.highAccuracy);
+ qCDebug(l) << "got pos timestamp" << pos.timestamp() << " but we want" << threshold;
+
+ if (pos.isValid() && pos.timestamp() >= threshold) {
+ invokeCallback(watcher.successCallback, buildPositionObject(pos));
+
+ if (once) {
+ return -1;
+ }
+ } else if (watcher.timeout == 0 && once) {
+ // If the timeout has already expired, and we have no cached data
+ // Do not even bother to turn on the GPS; return error object now.
+ invokeCallback(watcher.errorCallback, buildPositionErrorObject(TIMEOUT, "no cached position"));
+ return -1;
+ }
+ }
+
+ watcher.timer.start();
+ m_watchers.append(watcher);
+
+ qCDebug(l) << "added new watcher" << watcher.watcherId;
+ QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection);
+
+ return watcher.watcherId;
+}
+
+void JSKitGeolocation::removeWatcher(int watcherId)
+{
+ Watcher watcher;
+
+ qCDebug(l) << "removing watcherId" << watcher.watcherId;
+
+ for (int i = 0; i < m_watchers.size(); i++) {
+ if (m_watchers[i].watcherId == watcherId) {
+ watcher = m_watchers.takeAt(i);
+ break;
+ }
+ }
+
+ if (watcher.watcherId != watcherId) {
+ qCWarning(l) << "watcherId not found";
+ return;
+ }
+
+ QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection);
+}
+
+QJSValue JSKitGeolocation::buildPositionObject(const QGeoPositionInfo &pos)
+{
+ QJSValue obj = m_engine->newObject();
+ QJSValue coords = m_engine->newObject();
+ QJSValue timestamp = m_engine->toScriptValue<quint64>(pos.timestamp().toMSecsSinceEpoch());
+
+ coords.setProperty("latitude", m_engine->toScriptValue(pos.coordinate().latitude()));
+ coords.setProperty("longitude", m_engine->toScriptValue(pos.coordinate().longitude()));
+ if (pos.coordinate().type() == QGeoCoordinate::Coordinate3D) {
+ coords.setProperty("altitude", m_engine->toScriptValue(pos.coordinate().altitude()));
+ } else {
+ coords.setProperty("altitude", m_engine->toScriptValue<void*>(0));
+ }
+
+ coords.setProperty("accuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::HorizontalAccuracy)));
+
+ if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) {
+ coords.setProperty("altitudeAccuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::VerticalAccuracy)));
+ } else {
+ coords.setProperty("altitudeAccuracy", m_engine->toScriptValue<void*>(0));
+ }
+
+ if (pos.hasAttribute(QGeoPositionInfo::Direction)) {
+ coords.setProperty("heading", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::Direction)));
+ } else {
+ coords.setProperty("heading", m_engine->toScriptValue<void*>(0));
+ }
+
+ if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) {
+ coords.setProperty("speed", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::GroundSpeed)));
+ } else {
+ coords.setProperty("speed", m_engine->toScriptValue<void*>(0));
+ }
+
+ obj.setProperty("coords", coords);
+ obj.setProperty("timestamp", timestamp);
+
+ return obj;
+}
+
+QJSValue JSKitGeolocation::buildPositionErrorObject(PositionError error, const QString &message)
+{
+ QJSValue obj = m_engine->newObject();
+
+ obj.setProperty("code", m_engine->toScriptValue<unsigned short>(error));
+ obj.setProperty("message", m_engine->toScriptValue(message));
+
+ return obj;
+}
+
+void JSKitGeolocation::invokeCallback(QJSValue callback, QJSValue event)
+{
+ if (callback.isCallable()) {
+ qCDebug(l) << "invoking callback" << callback.toString();
+ QJSValue result = callback.call(QJSValueList({event}));
+
+ if (result.isError()) {
+ qCWarning(l) << "error while invoking callback: " << QString("%1:%2: %3")
+ .arg(result.property("fileName").toString())
+ .arg(result.property("lineNumber").toInt())
+ .arg(result.toString());
+ }
+ } else {
+ qCWarning(l) << "callback is not callable";
+ }
+}
+
+void JSKitGeolocation::stopAndRemove()
+{
+ if (m_source) {
+ qCDebug(l) << "removing source";
+
+ m_source->stopUpdates();
+ m_source->deleteLater();
+ m_source = 0;
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.h b/rockworkd/libpebble/jskit/jskitgeolocation.h
new file mode 100644
index 0000000..582ab32
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitgeolocation.h
@@ -0,0 +1,66 @@
+#ifndef JSKITGEOLOCATION_H
+#define JSKITGEOLOCATION_H
+
+#include <QElapsedTimer>
+#include <QGeoPositionInfoSource>
+#include <QJSValue>
+#include <QLoggingCategory>
+#include <QJSEngine>
+
+class JSKitGeolocation : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+ struct Watcher;
+
+public:
+ explicit JSKitGeolocation(QJSEngine *engine);
+
+ enum PositionError {
+ PERMISSION_DENIED = 1,
+ POSITION_UNAVAILABLE = 2,
+ TIMEOUT = 3
+ };
+ Q_ENUMS(PositionError);
+
+ Q_INVOKABLE void getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap());
+ Q_INVOKABLE int watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap());
+ Q_INVOKABLE void clearWatch(int watcherId);
+
+private slots:
+ void handleError(const QGeoPositionInfoSource::Error error);
+ void handlePosition(const QGeoPositionInfo &pos);
+ void handleTimeout();
+ void updateTimeouts();
+
+private:
+ int setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once);
+ void removeWatcher(int watcherId);
+
+ QJSValue buildPositionObject(const QGeoPositionInfo &pos);
+ QJSValue buildPositionErrorObject(PositionError error, const QString &message = QString());
+ QJSValue buildPositionErrorObject(const QGeoPositionInfoSource::Error error);
+ void invokeCallback(QJSValue callback, QJSValue event);
+ void stopAndRemove();
+
+private:
+ QJSEngine *m_engine;
+ QGeoPositionInfoSource *m_source;
+
+ struct Watcher {
+ QJSValue successCallback;
+ QJSValue errorCallback;
+ int watcherId;
+ bool once;
+ bool highAccuracy;
+ int timeout;
+ QElapsedTimer timer;
+ qlonglong maximumAge;
+ };
+
+ QList<Watcher> m_watchers;
+ int m_lastWatcherId;
+};
+
+#endif // JSKITGEOLOCATION_H
diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.cpp b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp
new file mode 100644
index 0000000..d69b6ad
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp
@@ -0,0 +1,117 @@
+#include <QDesktopServices>
+#include <QDir>
+#include <QDebug>
+
+#include "jskitlocalstorage.h"
+
+JSKitLocalStorage::JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid):
+ QObject(engine),
+ m_engine(engine),
+ m_storage(new QSettings(getStorageFileFor(storagePath, uuid), QSettings::IniFormat, this))
+{
+}
+
+int JSKitLocalStorage::length() const
+{
+ return m_storage->allKeys().size();
+}
+
+QJSValue JSKitLocalStorage::getItem(const QJSValue &key) const
+{
+ QVariant value = m_storage->value(key.toString());
+
+ if (value.isValid()) {
+ return QJSValue(value.toString());
+ } else {
+ return QJSValue(QJSValue::NullValue);
+ }
+}
+
+bool JSKitLocalStorage::setItem(const QJSValue &key, const QJSValue &value)
+{
+ m_storage->setValue(key.toString(), QVariant::fromValue(value.toString()));
+ return true;
+}
+
+bool JSKitLocalStorage::removeItem(const QJSValue &key)
+{
+ if (m_storage->contains(key.toString())) {
+ m_storage->remove(key.toString());
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void JSKitLocalStorage::clear()
+{
+ m_storage->clear();
+}
+
+QJSValue JSKitLocalStorage::key(int index)
+{
+ QStringList allKeys = m_storage->allKeys();
+ QJSValue key(QJSValue::NullValue);
+
+ if (allKeys.size() > index) {
+ key = QJSValue(allKeys[index]);
+ }
+
+ return key;
+}
+
+QJSValue JSKitLocalStorage::get(const QJSValue &proxy, const QJSValue &key) const
+{
+ Q_UNUSED(proxy);
+ return getItem(key);
+}
+
+bool JSKitLocalStorage::set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value)
+{
+ Q_UNUSED(proxy);
+ return setItem(key, value);
+}
+
+bool JSKitLocalStorage::has(const QJSValue &proxy, const QJSValue &key)
+{
+ Q_UNUSED(proxy);
+ return m_storage->contains(key.toString());
+}
+
+bool JSKitLocalStorage::deleteProperty(const QJSValue &proxy, const QJSValue &key)
+{
+ Q_UNUSED(proxy);
+ return removeItem(key);
+}
+
+QJSValue JSKitLocalStorage::keys(const QJSValue &proxy)
+{
+ Q_UNUSED(proxy);
+
+ QStringList allKeys = m_storage->allKeys();
+ QJSValue keyArray = m_engine->newArray(allKeys.size());
+ for (int i = 0; i < allKeys.size(); i++) {
+ keyArray.setProperty(i, allKeys[i]);
+ }
+
+ return keyArray;
+}
+
+QJSValue JSKitLocalStorage::enumerate()
+{
+ return keys(0);
+}
+
+QString JSKitLocalStorage::getStorageFileFor(const QString &storageDir, const QUuid &uuid)
+{
+ QDir dataDir(storageDir + "/js-storage");
+ if (!dataDir.exists() && !dataDir.mkpath(dataDir.absolutePath())) {
+ qWarning() << "Error creating jskit storage dir";
+ return QString();
+ }
+
+ QString fileName = uuid.toString();
+ fileName.remove('{');
+ fileName.remove('}');
+ return dataDir.absoluteFilePath(fileName + ".ini");
+}
diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.h b/rockworkd/libpebble/jskit/jskitlocalstorage.h
new file mode 100644
index 0000000..9719f83
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitlocalstorage.h
@@ -0,0 +1,40 @@
+#ifndef JSKITLOCALSTORAGE_P_H
+#define JSKITLOCALSTORAGE_P_H
+
+#include <QSettings>
+#include <QJSEngine>
+#include <QUuid>
+
+class JSKitLocalStorage : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int length READ length)
+
+public:
+ explicit JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid);
+
+ int length() const;
+
+ Q_INVOKABLE QJSValue getItem(const QJSValue &key) const;
+ Q_INVOKABLE bool setItem(const QJSValue &key, const QJSValue &value);
+ Q_INVOKABLE bool removeItem(const QJSValue &key);
+ Q_INVOKABLE void clear();
+ Q_INVOKABLE QJSValue key(int index);
+
+ Q_INVOKABLE QJSValue get(const QJSValue &proxy, const QJSValue &key) const;
+ Q_INVOKABLE bool set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value);
+ Q_INVOKABLE bool has(const QJSValue &proxy, const QJSValue &key);
+ Q_INVOKABLE bool deleteProperty(const QJSValue &proxy, const QJSValue &key);
+ Q_INVOKABLE QJSValue keys(const QJSValue &proxy=0);
+ Q_INVOKABLE QJSValue enumerate();
+
+private:
+ static QString getStorageFileFor(const QString &storageDir, const QUuid &uuid);
+
+private:
+ QJSEngine *m_engine;
+ QSettings *m_storage;
+};
+
+#endif // JSKITLOCALSTORAGE_P_H
diff --git a/rockworkd/libpebble/jskit/jskitmanager.cpp b/rockworkd/libpebble/jskit/jskitmanager.cpp
new file mode 100644
index 0000000..04bf674
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitmanager.cpp
@@ -0,0 +1,240 @@
+#include <QFile>
+#include <QDir>
+#include <QUrl>
+
+#include "jskitmanager.h"
+#include "jskitpebble.h"
+
+JSKitManager::JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent) :
+ QObject(parent),
+ l(metaObject()->className()),
+ m_pebble(pebble),
+ m_connection(connection),
+ m_apps(apps),
+ m_appmsg(appmsg),
+ m_engine(0),
+ m_configurationUuid(0)
+{
+ connect(m_appmsg, &AppMsgManager::appStarted, this, &JSKitManager::handleAppStarted);
+ connect(m_appmsg, &AppMsgManager::appStopped, this, &JSKitManager::handleAppStopped);
+}
+
+JSKitManager::~JSKitManager()
+{
+ if (m_engine) {
+ stopJsApp();
+ }
+}
+
+QJSEngine * JSKitManager::engine()
+{
+ return m_engine;
+}
+
+bool JSKitManager::isJSKitAppRunning() const
+{
+ return m_engine != 0;
+}
+
+QString JSKitManager::describeError(QJSValue error)
+{
+ return QString("%1:%2: %3")
+ .arg(error.property("fileName").toString())
+ .arg(error.property("lineNumber").toInt())
+ .arg(error.toString());
+}
+
+void JSKitManager::showConfiguration()
+{
+ if (m_engine) {
+ qCDebug(l) << "requesting configuration";
+ m_jspebble->invokeCallbacks("showConfiguration");
+ } else {
+ qCWarning(l) << "requested to show configuration, but JS engine is not running";
+ }
+}
+
+void JSKitManager::handleWebviewClosed(const QString &result)
+{
+ if (m_engine) {
+ QJSValue eventObj = m_engine->newObject();
+ eventObj.setProperty("response", QUrl::fromPercentEncoding(result.toUtf8()));
+
+ qCDebug(l) << "Sending" << eventObj.property("response").toString();
+ m_jspebble->invokeCallbacks("webviewclosed", QJSValueList({eventObj}));
+
+ loadJsFile(":/cacheLocalStorage.js");
+ } else {
+ qCWarning(l) << "webview closed event, but JS engine is not running";
+ }
+}
+
+void JSKitManager::setConfigurationId(const QUuid &uuid)
+{
+ m_configurationUuid = uuid;
+}
+
+AppInfo JSKitManager::currentApp()
+{
+ return m_curApp;
+}
+
+void JSKitManager::handleAppStarted(const QUuid &uuid)
+{
+ AppInfo info = m_apps->info(uuid);
+ if (!info.uuid().isNull() && info.isJSKit()) {
+ qCDebug(l) << "Preparing to start JSKit app" << info.uuid() << info.shortName();
+
+ m_curApp = info;
+ startJsApp();
+ }
+}
+
+void JSKitManager::handleAppStopped(const QUuid &uuid)
+{
+ if (!m_curApp.uuid().isNull()) {
+ if (m_curApp.uuid() != uuid) {
+ qCWarning(l) << "Closed app with invalid UUID";
+ }
+
+ stopJsApp();
+ m_curApp = AppInfo();
+ qCDebug(l) << "App stopped" << uuid;
+ }
+}
+
+void JSKitManager::handleAppMessage(const QUuid &uuid, const QVariantMap &msg)
+{
+ if (m_curApp.uuid() == uuid) {
+ qCDebug(l) << "handling app message" << uuid << msg;
+
+ if (m_engine) {
+ QJSValue eventObj = m_engine->newObject();
+ eventObj.setProperty("payload", m_engine->toScriptValue(msg));
+
+ m_jspebble->invokeCallbacks("appmessage", QJSValueList({eventObj}));
+
+ loadJsFile(":/cacheLocalStorage.js");
+ }
+ else {
+ qCDebug(l) << "but engine is stopped";
+ }
+ }
+}
+
+bool JSKitManager::loadJsFile(const QString &filename)
+{
+ Q_ASSERT(m_engine);
+
+ QFile file(filename);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qCWarning(l) << "Failed to load JS file:" << file.fileName();
+ return false;
+ }
+
+ qCDebug(l) << "evaluating js file" << file.fileName();
+
+ QJSValue result = m_engine->evaluate(QString::fromUtf8(file.readAll()), file.fileName());
+ if (result.isError()) {
+ qCWarning(l) << "error while evaluating JS script:" << describeError(result);
+ return false;
+ }
+
+ qCDebug(l) << "JS script evaluated";
+ return true;
+}
+
+void JSKitManager::startJsApp()
+{
+ if (m_engine) stopJsApp();
+
+ if (m_curApp.uuid().isNull()) {
+ qCWarning(l) << "Attempting to start JS app with invalid UUID";
+ return;
+ }
+
+ m_engine = new QJSEngine(this);
+ m_jspebble = new JSKitPebble(m_curApp, this, m_engine);
+ m_jsconsole = new JSKitConsole(m_engine);
+ m_jsstorage = new JSKitLocalStorage(m_engine, m_pebble->storagePath(), m_curApp.uuid());
+ m_jsgeo = new JSKitGeolocation(m_engine);
+ m_jstimer = new JSKitTimer(m_engine);
+ m_jsperformance = new JSKitPerformance(m_engine);
+
+ qCDebug(l) << "starting JS app" << m_curApp.shortName();
+
+ QJSValue globalObj = m_engine->globalObject();
+ QJSValue jskitObj = m_engine->newObject();
+
+ jskitObj.setProperty("pebble", m_engine->newQObject(m_jspebble));
+ jskitObj.setProperty("console", m_engine->newQObject(m_jsconsole));
+ jskitObj.setProperty("localstorage", m_engine->newQObject(m_jsstorage));
+ jskitObj.setProperty("geolocation", m_engine->newQObject(m_jsgeo));
+ jskitObj.setProperty("timer", m_engine->newQObject(m_jstimer));
+ jskitObj.setProperty("performance", m_engine->newQObject(m_jsperformance));
+ globalObj.setProperty("_jskit", jskitObj);
+
+ QJSValue navigatorObj = m_engine->newObject();
+ navigatorObj.setProperty("language", m_engine->toScriptValue(QLocale().name()));
+ globalObj.setProperty("navigator", navigatorObj);
+
+ // Set this.window = this
+ globalObj.setProperty("window", globalObj);
+
+ // Shims for compatibility...
+ loadJsFile(":/jskitsetup.js");
+
+ // Polyfills...
+ loadJsFile(":/typedarray.js");
+
+ // Now the actual script
+ QString jsApp = m_curApp.file(AppInfo::FileTypeJsApp, HardwarePlatformUnknown);
+ QFile f(jsApp);
+ if (!f.open(QFile::ReadOnly)) {
+ qCWarning(l) << "Error opening" << jsApp;
+ return;
+ }
+ QJSValue ret = m_engine->evaluate(QString::fromUtf8(f.readAll()));
+ qCDebug(l) << "loaded script" << ret.toString();
+
+ // Setup the message callback
+ QUuid uuid = m_curApp.uuid();
+ m_appmsg->setMessageHandler(uuid, [this, uuid](const QVariantMap &msg) {
+ QMetaObject::invokeMethod(this, "handleAppMessage", Qt::QueuedConnection,
+ Q_ARG(QUuid, uuid),
+ Q_ARG(QVariantMap, msg));
+
+ // Invoke the slot as a queued connection to give time for the ACK message
+ // to go through first.
+
+ return true;
+ });
+
+ // We try to invoke the callbacks even if script parsing resulted in error...
+ m_jspebble->invokeCallbacks("ready");
+
+ loadJsFile(":/cacheLocalStorage.js");
+
+ if (m_configurationUuid == m_curApp.uuid()) {
+ qCDebug(l) << "going to launch config for" << m_configurationUuid;
+ showConfiguration();
+ }
+
+ m_configurationUuid = QUuid();
+}
+
+void JSKitManager::stopJsApp()
+{
+ qCDebug(l) << "stop js app" << m_curApp.uuid();
+ if (!m_engine) return; // Nothing to do!
+
+ loadJsFile(":/cacheLocalStorage.js");
+
+ if (!m_curApp.uuid().isNull()) {
+ m_appmsg->clearMessageHandler(m_curApp.uuid());
+ }
+
+ m_engine->collectGarbage();
+ m_engine->deleteLater();
+ m_engine = 0;
+}
diff --git a/rockworkd/libpebble/jskit/jskitmanager.h b/rockworkd/libpebble/jskit/jskitmanager.h
new file mode 100644
index 0000000..570948e
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitmanager.h
@@ -0,0 +1,72 @@
+#ifndef JSKITMANAGER_H
+#define JSKITMANAGER_H
+
+#include <QJSEngine>
+#include <QPointer>
+#include <QLoggingCategory>
+
+#include "../appmanager.h"
+#include "../watchconnection.h"
+#include "../pebble.h"
+#include "../appmsgmanager.h"
+
+#include "jskitconsole.h"
+#include "jskitgeolocation.h"
+#include "jskitlocalstorage.h"
+#include "jskittimer.h"
+#include "jskitperformance.h"
+
+class JSKitPebble;
+
+class JSKitManager : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent = 0);
+ ~JSKitManager();
+
+ QJSEngine * engine();
+ bool isJSKitAppRunning() const;
+
+ static QString describeError(QJSValue error);
+
+ void showConfiguration();
+ void handleWebviewClosed(const QString &result);
+ void setConfigurationId(const QUuid &uuid);
+ AppInfo currentApp();
+
+signals:
+ void appNotification(const QUuid &uuid, const QString &title, const QString &body);
+ void openURL(const QString &uuid, const QString &url);
+
+private slots:
+ void handleAppStarted(const QUuid &uuid);
+ void handleAppStopped(const QUuid &uuid);
+ void handleAppMessage(const QUuid &uuid, const QVariantMap &msg);
+
+private:
+ bool loadJsFile(const QString &filename);
+ void startJsApp();
+ void stopJsApp();
+
+private:
+ friend class JSKitPebble;
+
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ AppManager *m_apps;
+ AppMsgManager *m_appmsg;
+ AppInfo m_curApp;
+ QJSEngine *m_engine;
+ QPointer<JSKitPebble> m_jspebble;
+ QPointer<JSKitConsole> m_jsconsole;
+ QPointer<JSKitLocalStorage> m_jsstorage;
+ QPointer<JSKitGeolocation> m_jsgeo;
+ QPointer<JSKitTimer> m_jstimer;
+ QPointer<JSKitPerformance> m_jsperformance;
+ QUuid m_configurationUuid;
+};
+
+#endif // JSKITMANAGER_H
diff --git a/rockworkd/libpebble/jskit/jskitpebble.cpp b/rockworkd/libpebble/jskit/jskitpebble.cpp
new file mode 100644
index 0000000..a300aef
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitpebble.cpp
@@ -0,0 +1,355 @@
+#include <QUrl>
+#include <QCryptographicHash>
+#include <QSettings>
+
+#include "jskitpebble.h"
+#include "jskitxmlhttprequest.h"
+
+static const char *token_salt = "0feeb7416d3c4546a19b04bccd8419b1";
+
+JSKitPebble::JSKitPebble(const AppInfo &info, JSKitManager *mgr, QObject *parent) :
+ QObject(parent),
+ l(metaObject()->className()),
+ m_appInfo(info),
+ m_mgr(mgr)
+{
+}
+
+void JSKitPebble::addEventListener(const QString &type, QJSValue function)
+{
+ m_listeners[type].append(function);
+}
+
+void JSKitPebble::removeEventListener(const QString &type, QJSValue function)
+{
+ if (!m_listeners.contains(type)) return;
+
+ QList<QJSValue> &callbacks = m_listeners[type];
+ for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ) {
+ if (it->strictlyEquals(function)) {
+ it = callbacks.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ if (callbacks.empty()) {
+ m_listeners.remove(type);
+ }
+}
+
+void JSKitPebble::showSimpleNotificationOnPebble(const QString &title, const QString &body)
+{
+ qCDebug(l) << "showSimpleNotificationOnPebble" << title << body;
+ emit m_mgr->appNotification(m_appInfo.uuid(), title, body);
+}
+
+uint JSKitPebble::sendAppMessage(QJSValue message, QJSValue callbackForAck, QJSValue callbackForNack)
+{
+ QVariantMap data = message.toVariant().toMap();
+ QPointer<JSKitPebble> pebbObj = this;
+ uint transactionId = m_mgr->m_appmsg->nextTransactionId();
+
+ qCDebug(l) << "sendAppMessage" << data;
+
+ m_mgr->m_appmsg->send(
+ m_appInfo.uuid(),
+ data,
+ [this, pebbObj, transactionId, callbackForAck]() mutable {
+ if (pebbObj.isNull()) return;
+
+ if (callbackForAck.isCallable()) {
+ QJSValue event = pebbObj->buildAckEventObject(transactionId);
+ QJSValue result = callbackForAck.call(QJSValueList({event}));
+
+ if (result.isError()) {
+ qCWarning(l) << "error while invoking ACK callback"
+ << callbackForAck.toString() << ":"
+ << JSKitManager::describeError(result);
+ }
+ }
+ },
+ [this, pebbObj, transactionId, callbackForNack]() mutable {
+ if (pebbObj.isNull()) return;
+
+ if (callbackForNack.isCallable()) {
+ QJSValue event = pebbObj->buildAckEventObject(transactionId, "NACK from watch");
+ QJSValue result = callbackForNack.call(QJSValueList({event}));
+
+ if (result.isError()) {
+ qCWarning(l) << "error while invoking NACK callback"
+ << callbackForNack.toString() << ":"
+ << JSKitManager::describeError(result);
+ }
+ }
+ }
+ );
+
+ return transactionId;
+}
+
+void JSKitPebble::getTimelineToken(QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.getTimelineToken";
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+void JSKitPebble::timelineSubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.timelineSubscribe";
+ Q_UNUSED(topic);
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+void JSKitPebble::timelineUnsubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.timelineUnsubscribe";
+ Q_UNUSED(topic);
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+void JSKitPebble::timelineSubscriptions(QJSValue successCallback, QJSValue failureCallback)
+{
+ //TODO actually implement this
+ qCDebug(l) << "call to unsupported method Pebble.timelineSubscriptions";
+ Q_UNUSED(successCallback);
+
+ if (failureCallback.isCallable()) {
+ failureCallback.call();
+ }
+}
+
+
+QString JSKitPebble::getAccountToken() const
+{
+ // We do not have any account system, so we just fake something up.
+ QCryptographicHash hasher(QCryptographicHash::Md5);
+
+ hasher.addData(token_salt, strlen(token_salt));
+ hasher.addData(m_appInfo.uuid().toByteArray());
+
+ QSettings settings;
+ QString token = settings.value("accountToken").toString();
+
+ if (token.isEmpty()) {
+ token = QUuid::createUuid().toString();
+ qCDebug(l) << "created new account token" << token;
+ settings.setValue("accountToken", token);
+ }
+
+ hasher.addData(token.toLatin1());
+
+ QString hash = hasher.result().toHex();
+ qCDebug(l) << "returning account token" << hash;
+
+ return hash;
+}
+
+QString JSKitPebble::getWatchToken() const
+{
+ QCryptographicHash hasher(QCryptographicHash::Md5);
+
+ hasher.addData(token_salt, strlen(token_salt));
+ hasher.addData(m_appInfo.uuid().toByteArray());
+ hasher.addData(m_mgr->m_pebble->serialNumber().toLatin1());
+
+ QString hash = hasher.result().toHex();
+ qCDebug(l) << "returning watch token" << hash;
+
+ return hash;
+}
+
+QJSValue JSKitPebble::getActiveWatchInfo() const
+{
+ QJSValue watchInfo = m_mgr->m_engine->newObject();
+
+ switch (m_mgr->m_pebble->hardwarePlatform()) {
+ case HardwarePlatformBasalt:
+ watchInfo.setProperty("platform", "basalt");
+ break;
+
+ case HardwarePlatformChalk:
+ watchInfo.setProperty("platform", "chalk");
+ break;
+
+ default:
+ watchInfo.setProperty("platform", "aplite");
+ break;
+ }
+
+ switch (m_mgr->m_pebble->model()) {
+ case ModelTintinWhite:
+ watchInfo.setProperty("model", "pebble_white");
+ break;
+
+ case ModelTintinRed:
+ watchInfo.setProperty("model", "pebble_red");
+ break;
+
+ case ModelTintinOrange:
+ watchInfo.setProperty("model", "pebble_orange");
+ break;
+
+ case ModelTintinGrey:
+ watchInfo.setProperty("model", "pebble_grey");
+ break;
+
+ case ModelBiancaSilver:
+ watchInfo.setProperty("model", "pebble_steel_silver");
+ break;
+
+ case ModelBiancaBlack:
+ watchInfo.setProperty("model", "pebble_steel_black");
+ break;
+
+ case ModelTintinBlue:
+ watchInfo.setProperty("model", "pebble_blue");
+ break;
+
+ case ModelTintinGreen:
+ watchInfo.setProperty("model", "pebble_green");
+ break;
+
+ case ModelTintinPink:
+ watchInfo.setProperty("model", "pebble_pink");
+ break;
+
+ case ModelSnowyWhite:
+ watchInfo.setProperty("model", "pebble_time_white");
+ break;
+
+ case ModelSnowyBlack:
+ watchInfo.setProperty("model", "pebble_time_black");
+ break;
+
+ case ModelSnowyRed:
+ watchInfo.setProperty("model", "pebble_time_read");
+ break;
+
+ case ModelBobbySilver:
+ watchInfo.setProperty("model", "pebble_time_steel_silver");
+ break;
+
+ case ModelBobbyBlack:
+ watchInfo.setProperty("model", "pebble_time_steel_black");
+ break;
+
+ case ModelBobbyGold:
+ watchInfo.setProperty("model", "pebble_time_steel_gold");
+ break;
+
+ case ModelSpalding14Silver:
+ watchInfo.setProperty("model", "pebble_time_round_silver_14mm");
+ break;
+
+ case ModelSpalding14Black:
+ watchInfo.setProperty("model", "pebble_time_round_black_14mm");
+ break;
+
+ case ModelSpalding20Silver:
+ watchInfo.setProperty("model", "pebble_time_round_silver_20mm");
+ break;
+
+ case ModelSpalding20Black:
+ watchInfo.setProperty("model", "pebble_time_round_black_20mm");
+ break;
+
+ case ModelSpalding14RoseGold:
+ watchInfo.setProperty("model", "pebble_time_round_rose_gold_14mm");
+ break;
+
+ default:
+ watchInfo.setProperty("model", "pebble_black");
+ break;
+ }
+
+ watchInfo.setProperty("language", m_mgr->m_pebble->language());
+
+ QJSValue firmware = m_mgr->m_engine->newObject();
+ QString version = m_mgr->m_pebble->softwareVersion().remove("v");
+ QStringList versionParts = version.split(".");
+
+ if (versionParts.count() >= 1) {
+ firmware.setProperty("major", versionParts[0].toInt());
+ }
+
+ if (versionParts.count() >= 2) {
+ firmware.setProperty("minor", versionParts[1].toInt());
+ }
+
+ if (versionParts.count() >= 3) {
+ if (versionParts[2].contains("-")) {
+ QStringList patchParts = version.split("-");
+ firmware.setProperty("patch", patchParts[0].toInt());
+ firmware.setProperty("suffix", patchParts[1]);
+ } else {
+ firmware.setProperty("patch", versionParts[2].toInt());
+ firmware.setProperty("suffix", "");
+ }
+ }
+
+ watchInfo.setProperty("firmware", firmware);
+ return watchInfo;
+}
+
+void JSKitPebble::openURL(const QUrl &url)
+{
+ emit m_mgr->openURL(m_appInfo.uuid().toString(), url.toString());
+}
+
+QJSValue JSKitPebble::createXMLHttpRequest()
+{
+ JSKitXMLHttpRequest *xhr = new JSKitXMLHttpRequest(m_mgr->engine());
+ // Should be deleted by JS engine.
+ return m_mgr->engine()->newQObject(xhr);
+}
+
+QJSValue JSKitPebble::buildAckEventObject(uint transaction, const QString &message) const
+{
+ QJSEngine *engine = m_mgr->engine();
+ QJSValue eventObj = engine->newObject();
+ QJSValue dataObj = engine->newObject();
+
+ dataObj.setProperty("transactionId", engine->toScriptValue(transaction));
+ eventObj.setProperty("data", dataObj);
+
+ if (!message.isEmpty()) {
+ QJSValue errorObj = engine->newObject();
+
+ errorObj.setProperty("message", engine->toScriptValue(message));
+ eventObj.setProperty("error", errorObj);
+ }
+
+ return eventObj;
+}
+
+void JSKitPebble::invokeCallbacks(const QString &type, const QJSValueList &args)
+{
+ if (!m_listeners.contains(type)) return;
+ QList<QJSValue> &callbacks = m_listeners[type];
+
+ for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ++it) {
+ qCDebug(l) << "invoking callback" << type << it->toString();
+ QJSValue result = it->call(args);
+ if (result.isError()) {
+ qCWarning(l) << "error while invoking callback"
+ << type << it->toString() << ":"
+ << JSKitManager::describeError(result);
+ }
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskitpebble.h b/rockworkd/libpebble/jskit/jskitpebble.h
new file mode 100644
index 0000000..d9cd670
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitpebble.h
@@ -0,0 +1,47 @@
+#ifndef JSKITPEBBLE_P_H
+#define JSKITPEBBLE_P_H
+
+#include <QLoggingCategory>
+
+#include "jskitmanager.h"
+#include "../appinfo.h"
+
+class JSKitPebble : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitPebble(const AppInfo &appInfo, JSKitManager *mgr, QObject *parent=0);
+
+ Q_INVOKABLE void addEventListener(const QString &type, QJSValue function);
+ Q_INVOKABLE void removeEventListener(const QString &type, QJSValue function);
+
+ Q_INVOKABLE void showSimpleNotificationOnPebble(const QString &title, const QString &body);
+ Q_INVOKABLE uint sendAppMessage(QJSValue message, QJSValue callbackForAck = QJSValue(), QJSValue callbackForNack = QJSValue());
+
+ Q_INVOKABLE void getTimelineToken(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+ Q_INVOKABLE void timelineSubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+ Q_INVOKABLE void timelineUnsubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+ Q_INVOKABLE void timelineSubscriptions(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue());
+
+ Q_INVOKABLE QString getAccountToken() const;
+ Q_INVOKABLE QString getWatchToken() const;
+ Q_INVOKABLE QJSValue getActiveWatchInfo() const;
+
+ Q_INVOKABLE void openURL(const QUrl &url);
+
+ Q_INVOKABLE QJSValue createXMLHttpRequest();
+
+ void invokeCallbacks(const QString &type, const QJSValueList &args = QJSValueList());
+
+private:
+ QJSValue buildAckEventObject(uint transaction, const QString &message = QString()) const;
+
+private:
+ AppInfo m_appInfo;
+ JSKitManager *m_mgr;
+ QHash<QString, QList<QJSValue>> m_listeners;
+};
+
+#endif // JSKITPEBBLE_P_H
diff --git a/rockworkd/libpebble/jskit/jskitperformance.cpp b/rockworkd/libpebble/jskit/jskitperformance.cpp
new file mode 100644
index 0000000..23b0e08
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitperformance.cpp
@@ -0,0 +1,13 @@
+#include "jskitperformance.h"
+
+JSKitPerformance::JSKitPerformance(QObject *parent) :
+ QObject(parent),
+ m_start(QTime::currentTime())
+{
+}
+
+int JSKitPerformance::now()
+{
+ QTime now = QTime::currentTime();
+ return m_start.msecsTo(now);
+}
diff --git a/rockworkd/libpebble/jskit/jskitperformance.h b/rockworkd/libpebble/jskit/jskitperformance.h
new file mode 100644
index 0000000..5f118be
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitperformance.h
@@ -0,0 +1,20 @@
+#ifndef JSKITPERFORMANCE_H
+#define JSKITPERFORMANCE_H
+
+#include <QObject>
+#include <QTime>
+
+class JSKitPerformance : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit JSKitPerformance(QObject *parent=0);
+
+ Q_INVOKABLE int now();
+
+private:
+ QTime m_start;
+};
+
+#endif // JSKITPERFORMANCE_H
diff --git a/rockworkd/libpebble/jskit/jskitsetup.js b/rockworkd/libpebble/jskit/jskitsetup.js
new file mode 100644
index 0000000..340c4f1
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitsetup.js
@@ -0,0 +1,196 @@
+//Borrowed from https://github.com/pebble/pypkjs/blob/master/pypkjs/javascript/runtime.py#L17
+_jskit.make_proxies = function(proxy, origin, names) {
+ names.forEach(function(name) {
+ proxy[name] = eval("(function " + name + "() { return origin[name].apply(origin, arguments); })");
+ });
+
+ return proxy;
+}
+
+_jskit.make_properties = function(proxy, origin, names) {
+ names.forEach(function(name) {
+ Object.defineProperty(proxy, name, {
+ configurable: false,
+ enumerable: true,
+ get: function() {
+ return origin[name];
+ },
+ set: function(value) {
+ origin[name] = value;
+ }
+ });
+ });
+
+ return proxy;
+}
+
+Pebble = new (function() {
+ _jskit.make_proxies(this, _jskit.pebble,
+ ['sendAppMessage', 'showSimpleNotificationOnPebble', 'getAccountToken', 'getWatchToken',
+ 'addEventListener', 'removeEventListener', 'openURL', 'getTimelineToken', 'timelineSubscribe',
+ 'timelineUnsubscribe', 'timelineSubscriptions', 'getActiveWatchInfo']
+ );
+})();
+
+performance = new (function() {
+ _jskit.make_proxies(this, _jskit.performance, ['now']);
+})();
+
+function XMLHttpRequest() {
+ var xhr = _jskit.pebble.createXMLHttpRequest();
+ _jskit.make_proxies(this, xhr,
+ ['open', 'setRequestHeader', 'overrideMimeType', 'send', 'getResponseHeader',
+ 'getAllResponseHeaders', 'abort', 'addEventListener', 'removeEventListener']);
+ _jskit.make_properties(this, xhr,
+ ['readyState', 'response', 'responseText', 'responseType', 'status',
+ 'statusText', 'timeout', 'onreadystatechange', 'ontimeout', 'onload',
+ 'onloadstart', 'onloadend', 'onprogress', 'onerror', 'onabort']);
+
+ this.UNSENT = 0;
+ this.OPENED = 1;
+ this.HEADERS_RECEIVED = 2;
+ this.LOADING = 3;
+ this.DONE = 4;
+}
+
+function setInterval(func, time) {
+ return _jskit.timer.setInterval(func, time);
+}
+
+function clearInterval(id) {
+ _jskit.timer.clearInterval(id);
+}
+
+function setTimeout(func, time) {
+ return _jskit.timer.setTimeout(func, time);
+}
+
+function clearTimeout(id) {
+ _jskit.timer.clearTimeout(id);
+}
+
+navigator.geolocation = new (function() {
+ _jskit.make_proxies(this, _jskit.geolocation,
+ ['getCurrentPosition', 'watchPosition', 'clearWatch']
+ );
+})();
+
+console = new (function() {
+ _jskit.make_proxies(this, _jskit.console,
+ ['log', 'warn', 'error', 'info']
+ );
+})();
+
+/*localStorage = new (function() {
+ _jskit.make_proxies(this, _jskit.localstorage,
+ ['clear', 'getItem', 'setItem', 'removeItem', 'key']
+ );
+
+ _jskit.make_properties(this, _jskit.localstorage,
+ ['length']
+ );
+})();*/
+
+//It appears that Proxy is not available since Qt is using Javascript v5
+/*(function() {
+ var proxy = _jskit.make_proxies({}, _jskit.localstorage, ['set', 'has', 'deleteProperty', 'keys', 'enumerate']);
+ var methods = _jskit.make_proxies({}, _jskit.localstorage, ['clear', 'getItem', 'setItem', 'removeItem', 'key']);
+ proxy.get = function get(p, name) { return methods[name] || _jskit.localstorage.get(p, name); }
+ this.localStorage = Proxy.create(proxy);
+})();*/
+
+//inspired by https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage
+Object.defineProperty(window, "localStorage", new (function () {
+ var storage = {};
+ Object.defineProperty(storage, "getItem", {
+ value: function (key) {
+ var value = null;
+ if (key !== undefined && key !== null && storage[key] !== undefined) {
+ value = storage[key];
+ }
+
+ return value;
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "key", {
+ value: function (index) {
+ return Object.keys(storage)[index];
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "setItem", {
+ value: function (key, value) {
+ if (key !== undefined && key !== null) {
+ _jskit.localstorage.setItem(key, value);
+ storage[key] = (value && value.toString) ? value.toString() : value;
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "length", {
+ get: function () {
+ return Object.keys(storage).length;
+ },
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "removeItem", {
+ value: function (key) {
+ if (key && storage[key]) {
+ _jskit.localstorage.removeItem(key);
+ delete storage[key];
+
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(storage, "clear", {
+ value: function (key) {
+ for (var key in storage) {
+ storage.removeItem(key);
+ }
+
+ return true;
+ },
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ this.get = function () {
+ return storage;
+ };
+
+ this.configurable = false;
+ this.enumerable = true;
+})());
+
+(function() {
+ var keys = _jskit.localstorage.keys();
+ for (var index in keys) {
+ var value = _jskit.localstorage.getItem(keys[index]);
+ localStorage.setItem(keys[index], value);
+ }
+})();
diff --git a/rockworkd/libpebble/jskit/jskittimer.cpp b/rockworkd/libpebble/jskit/jskittimer.cpp
new file mode 100644
index 0000000..6ab5b4a
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskittimer.cpp
@@ -0,0 +1,77 @@
+#include <QTimerEvent>
+
+#include "jskittimer.h"
+
+JSKitTimer::JSKitTimer(QJSEngine *engine) :
+ QObject(engine),
+ l(metaObject()->className()),
+ m_engine(engine)
+{
+}
+
+int JSKitTimer::setInterval(QJSValue expression, int delay) //TODO support optional parameters
+{
+ qCDebug(l) << "Setting interval for " << delay << "ms: " << expression.toString();
+
+ if (expression.isString() || expression.isCallable()) {
+ int timerId = startTimer(delay);
+ m_intervals.insert(timerId, expression);
+
+ return timerId;
+ }
+
+ return -1;
+}
+
+void JSKitTimer::clearInterval(int timerId)
+{
+ qCDebug(l) << "Killing interval " << timerId ;
+ killTimer(timerId);
+ m_intervals.remove(timerId);
+}
+
+int JSKitTimer::setTimeout(QJSValue expression, int delay) //TODO support optional parameters
+{
+ qCDebug(l) << "Setting timeout for " << delay << "ms: " << expression.toString();
+
+ if (expression.isString() || expression.isCallable()) {
+ int timerId = startTimer(delay);
+ m_timeouts.insert(timerId, expression);
+
+ return timerId;
+ }
+
+ return -1;
+}
+
+void JSKitTimer::clearTimeout(int timerId)
+{
+ qCDebug(l) << "Killing timeout " << timerId ;
+ killTimer(timerId);
+ m_timeouts.remove(timerId);
+}
+
+void JSKitTimer::timerEvent(QTimerEvent *event)
+{
+ int id = event->timerId();
+
+ QJSValue expression; // find in either intervals or timeouts
+ if (m_intervals.contains(id)) {
+ expression = m_intervals.value(id);
+ } else if (m_timeouts.contains(id)) {
+ expression = m_timeouts.value(id);
+ killTimer(id); // timeouts don't repeat
+ } else {
+ qCWarning(l) << "Unknown timer event";
+ killTimer(id); // interval nor timeout exist. kill the timer
+
+ return;
+ }
+
+ if (expression.isCallable()) { // call it if it's a function
+ expression.call().toString();
+ }
+ else { // otherwise evaluate it
+ m_engine->evaluate(expression.toString());
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskittimer.h b/rockworkd/libpebble/jskit/jskittimer.h
new file mode 100644
index 0000000..50b394d
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskittimer.h
@@ -0,0 +1,31 @@
+#ifndef JSKITTIMER_P_H
+#define JSKITTIMER_P_H
+
+#include <QLoggingCategory>
+#include <QJSValue>
+#include <QJSEngine>
+
+class JSKitTimer : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+public:
+ explicit JSKitTimer(QJSEngine *engine);
+
+ Q_INVOKABLE int setInterval(QJSValue expression, int delay);
+ Q_INVOKABLE void clearInterval(int timerId);
+
+ Q_INVOKABLE int setTimeout(QJSValue expression, int delay);
+ Q_INVOKABLE void clearTimeout(int timerId);
+
+protected:
+ void timerEvent(QTimerEvent *event);
+
+private:
+ QJSEngine *m_engine;
+ QHash<int, QJSValue> m_intervals;
+ QHash<int, QJSValue> m_timeouts;
+};
+
+#endif // JSKITTIMER_P_H
diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp
new file mode 100644
index 0000000..5948683
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp
@@ -0,0 +1,318 @@
+#include <QBuffer>
+#include <QAuthenticator>
+#include <QEventLoop>
+
+#include "jskitxmlhttprequest.h"
+#include "jskitmanager.h"
+
+JSKitXMLHttpRequest::JSKitXMLHttpRequest(QJSEngine *engine) :
+ QObject(engine),
+ l(metaObject()->className()),
+ m_engine(engine),
+ m_net(new QNetworkAccessManager(this)),
+ m_timeout(0),
+ m_reply(0)
+{
+ connect(m_net, &QNetworkAccessManager::authenticationRequired,
+ this, &JSKitXMLHttpRequest::handleAuthenticationRequired);
+}
+
+void JSKitXMLHttpRequest::open(const QString &method, const QString &url, bool async, const QString &username, const QString &password)
+{
+ if (m_reply) {
+ m_reply->deleteLater();
+ m_reply = 0;
+ }
+
+ m_username = username;
+ m_password = password;
+ m_request = QNetworkRequest(QUrl(url));
+ m_verb = method;
+ m_async = async;
+
+ qCDebug(l) << "opened to URL" << m_request.url().toString() << "Async:" << async;
+}
+
+void JSKitXMLHttpRequest::setRequestHeader(const QString &header, const QString &value)
+{
+ qCDebug(l) << "setRequestHeader" << header << value;
+ m_request.setRawHeader(header.toLatin1(), value.toLatin1());
+}
+
+void JSKitXMLHttpRequest::send(const QJSValue &data)
+{
+ QByteArray byteData;
+
+ if (data.isUndefined() || data.isNull()) {
+ // Do nothing, byteData is empty.
+ } else if (data.isString()) {
+ byteData = data.toString().toUtf8();
+ } else if (data.isObject()) {
+ if (data.hasProperty("byteLength")) {
+ // Looks like an ArrayView or an ArrayBufferView!
+ QJSValue buffer = data.property("buffer");
+ if (buffer.isUndefined()) {
+ // We must assume we've been passed an ArrayBuffer directly
+ buffer = data;
+ }
+
+ QJSValue array = data.property("_bytes");
+ int byteLength = data.property("byteLength").toInt();
+
+ if (array.isArray()) {
+ byteData.reserve(byteLength);
+
+ for (int i = 0; i < byteLength; i++) {
+ byteData.append(array.property(i).toInt());
+ }
+
+ qCDebug(l) << "passed an ArrayBufferView of" << byteData.length() << "bytes";
+ } else {
+ qCWarning(l) << "passed an unknown/invalid ArrayBuffer" << data.toString();
+ }
+ } else {
+ qCWarning(l) << "passed an unknown object" << data.toString();
+ }
+
+ }
+
+ QBuffer *buffer;
+ if (!byteData.isEmpty()) {
+ buffer = new QBuffer;
+ buffer->setData(byteData);
+ } else {
+ buffer = 0;
+ }
+
+ qCDebug(l) << "sending" << m_verb << "to" << m_request.url() << "with" << QString::fromUtf8(byteData);
+ m_reply = m_net->sendCustomRequest(m_request, m_verb.toLatin1(), buffer);
+
+ connect(m_reply, &QNetworkReply::finished,
+ this, &JSKitXMLHttpRequest::handleReplyFinished);
+ connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
+ this, &JSKitXMLHttpRequest::handleReplyError);
+
+ if (buffer) {
+ // So that it gets deleted alongside the reply object.
+ buffer->setParent(m_reply);
+ }
+
+ if (!m_async) {
+ QEventLoop loop; //Hacky way to get QNetworkReply be synchronous
+
+ connect(m_reply, &QNetworkReply::finished,
+ &loop, &QEventLoop::quit);
+ connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
+ &loop, &QEventLoop::quit);
+
+ loop.exec();
+ }
+}
+
+void JSKitXMLHttpRequest::abort()
+{
+ if (m_reply) {
+ m_reply->deleteLater();
+ m_reply = 0;
+ }
+}
+
+QJSValue JSKitXMLHttpRequest::onload() const
+{
+ return m_onload;
+}
+
+void JSKitXMLHttpRequest::setOnload(const QJSValue &value)
+{
+ m_onload = value;
+}
+
+QJSValue JSKitXMLHttpRequest::onreadystatechange() const
+{
+ return m_onreadystatechange;
+}
+
+void JSKitXMLHttpRequest::setOnreadystatechange(const QJSValue &value)
+{
+ m_onreadystatechange = value;
+}
+
+QJSValue JSKitXMLHttpRequest::ontimeout() const
+{
+ return m_ontimeout;
+}
+
+void JSKitXMLHttpRequest::setOntimeout(const QJSValue &value)
+{
+ m_ontimeout = value;
+}
+
+QJSValue JSKitXMLHttpRequest::onerror() const
+{
+ return m_onerror;
+}
+
+void JSKitXMLHttpRequest::setOnerror(const QJSValue &value)
+{
+ m_onerror = value;
+}
+
+uint JSKitXMLHttpRequest::readyState() const
+{
+ if (!m_reply) {
+ return UNSENT;
+ } else if (m_reply->isFinished()) {
+ return DONE;
+ } else {
+ return LOADING;
+ }
+}
+
+uint JSKitXMLHttpRequest::timeout() const
+{
+ return m_timeout;
+}
+
+void JSKitXMLHttpRequest::setTimeout(uint value)
+{
+ m_timeout = value;
+ // TODO Handle fetch in-progress.
+}
+
+uint JSKitXMLHttpRequest::status() const
+{
+ if (!m_reply || !m_reply->isFinished()) {
+ return 0;
+ } else {
+ return m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt();
+ }
+}
+
+QString JSKitXMLHttpRequest::statusText() const
+{
+ if (!m_reply || !m_reply->isFinished()) {
+ return QString();
+ } else {
+ return m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+ }
+}
+
+QString JSKitXMLHttpRequest::responseType() const
+{
+ return m_responseType;
+}
+
+void JSKitXMLHttpRequest::setResponseType(const QString &type)
+{
+ qCDebug(l) << "response type set to" << type;
+ m_responseType = type;
+}
+
+QJSValue JSKitXMLHttpRequest::response() const
+{
+ if (m_responseType.isEmpty() || m_responseType == "text") {
+ return m_engine->toScriptValue(QString::fromUtf8(m_response));
+ } else if (m_responseType == "arraybuffer") {
+ QJSValue arrayBufferProto = m_engine->globalObject().property("ArrayBuffer").property("prototype");
+ QJSValue arrayBuf = m_engine->newObject();
+
+ if (!arrayBufferProto.isUndefined()) {
+ arrayBuf.setPrototype(arrayBufferProto);
+ arrayBuf.setProperty("byteLength", m_engine->toScriptValue<uint>(m_response.size()));
+
+ QJSValue array = m_engine->newArray(m_response.size());
+ for (int i = 0; i < m_response.size(); i++) {
+ array.setProperty(i, m_engine->toScriptValue<int>(m_response[i]));
+ }
+
+ arrayBuf.setProperty("_bytes", array);
+ qCDebug(l) << "returning ArrayBuffer of" << m_response.size() << "bytes";
+ } else {
+ qCWarning(l) << "Cannot find proto of ArrayBuffer";
+ }
+
+ return arrayBuf;
+ } else {
+ qCWarning(l) << "unsupported responseType:" << m_responseType;
+ return m_engine->toScriptValue<void*>(0);
+ }
+}
+
+QString JSKitXMLHttpRequest::responseText() const
+{
+ return QString::fromUtf8(m_response);
+}
+
+void JSKitXMLHttpRequest::handleReplyFinished()
+{
+ if (!m_reply) {
+ qCDebug(l) << "reply finished too late";
+ return;
+ }
+
+ m_response = m_reply->readAll();
+ qCDebug(l) << "reply finished, reply text:" << QString::fromUtf8(m_response) << "status:" << status();
+
+ emit readyStateChanged();
+ emit statusChanged();
+ emit statusTextChanged();
+ emit responseChanged();
+ emit responseTextChanged();
+
+ if (m_onload.isCallable()) {
+ qCDebug(l) << "going to call onload handler:" << m_onload.toString();
+
+ QJSValue result = m_onload.callWithInstance(m_engine->newQObject(this));
+ if (result.isError()) {
+ qCWarning(l) << "JS error on onload handler:" << JSKitManager::describeError(result);
+ }
+ } else {
+ qCDebug(l) << "No onload set";
+ }
+
+ if (m_onreadystatechange.isCallable()) {
+ qCDebug(l) << "going to call onreadystatechange handler:" << m_onreadystatechange.toString();
+ QJSValue result = m_onreadystatechange.callWithInstance(m_engine->newQObject(this));
+ if (result.isError()) {
+ qCWarning(l) << "JS error on onreadystatechange handler:" << JSKitManager::describeError(result);
+ }
+ }
+}
+
+void JSKitXMLHttpRequest::handleReplyError(QNetworkReply::NetworkError code)
+{
+ if (!m_reply) {
+ qCDebug(l) << "reply error too late";
+ return;
+ }
+
+ qCDebug(l) << "reply error" << code;
+
+ emit readyStateChanged();
+ emit statusChanged();
+ emit statusTextChanged();
+
+ if (m_onerror.isCallable()) {
+ qCDebug(l) << "going to call onerror handler:" << m_onload.toString();
+ QJSValue result = m_onerror.callWithInstance(m_engine->newQObject(this));
+ if (result.isError()) {
+ qCWarning(l) << "JS error on onerror handler:" << JSKitManager::describeError(result);
+ }
+ }
+}
+
+void JSKitXMLHttpRequest::handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth)
+{
+ if (m_reply == reply) {
+ qCDebug(l) << "authentication required";
+
+ if (!m_username.isEmpty() || !m_password.isEmpty()) {
+ qCDebug(l) << "using provided authorization:" << m_username;
+
+ auth->setUser(m_username);
+ auth->setPassword(m_password);
+ } else {
+ qCDebug(l) << "no username or password provided";
+ }
+ }
+}
diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.h b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h
new file mode 100644
index 0000000..70b8136
--- /dev/null
+++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h
@@ -0,0 +1,96 @@
+#ifndef JSKITXMLHTTPREQUEST_P_H
+#define JSKITXMLHTTPREQUEST_P_H
+
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QJSEngine>
+#include <QLoggingCategory>
+
+class JSKitXMLHttpRequest : public QObject
+{
+ Q_OBJECT
+ QLoggingCategory l;
+
+ Q_PROPERTY(QJSValue onload READ onload WRITE setOnload)
+ Q_PROPERTY(QJSValue onreadystatechange READ onreadystatechange WRITE setOnreadystatechange)
+ Q_PROPERTY(QJSValue ontimeout READ ontimeout WRITE setOntimeout)
+ Q_PROPERTY(QJSValue onerror READ onerror WRITE setOnerror)
+ Q_PROPERTY(uint readyState READ readyState NOTIFY readyStateChanged)
+ Q_PROPERTY(uint timeout READ timeout WRITE setTimeout)
+ Q_PROPERTY(uint status READ status NOTIFY statusChanged)
+ Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged)
+ Q_PROPERTY(QString responseType READ responseType WRITE setResponseType)
+ Q_PROPERTY(QJSValue response READ response NOTIFY responseChanged)
+ Q_PROPERTY(QString responseText READ responseText NOTIFY responseTextChanged)
+
+public:
+ explicit JSKitXMLHttpRequest(QJSEngine *engine);
+
+ enum ReadyStates {
+ UNSENT = 0,
+ OPENED = 1,
+ HEADERS_RECEIVED = 2,
+ LOADING = 3,
+ DONE = 4
+ };
+ Q_ENUMS(ReadyStates)
+
+ Q_INVOKABLE void open(const QString &method, const QString &url, bool async = true, const QString &username = QString(), const QString &password = QString());
+ Q_INVOKABLE void setRequestHeader(const QString &header, const QString &value);
+ Q_INVOKABLE void send(const QJSValue &data = QJSValue(QJSValue::NullValue));
+ Q_INVOKABLE void abort();
+
+ QJSValue onload() const;
+ void setOnload(const QJSValue &value);
+ QJSValue onreadystatechange() const;
+ void setOnreadystatechange(const QJSValue &value);
+ QJSValue ontimeout() const;
+ void setOntimeout(const QJSValue &value);
+ QJSValue onerror() const;
+ void setOnerror(const QJSValue &value);
+
+ uint readyState() const;
+
+ uint timeout() const;
+ void setTimeout(uint value);
+
+ uint status() const;
+ QString statusText() const;
+
+ QString responseType() const;
+ void setResponseType(const QString& type);
+
+ QJSValue response() const;
+ QString responseText() const;
+
+signals:
+ void readyStateChanged();
+ void statusChanged();
+ void statusTextChanged();
+ void responseChanged();
+ void responseTextChanged();
+
+private slots:
+ void handleReplyFinished();
+ void handleReplyError(QNetworkReply::NetworkError code);
+ void handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth);
+
+private:
+ QJSEngine *m_engine;
+ QNetworkAccessManager *m_net;
+ QString m_verb;
+ bool m_async = true;
+ uint m_timeout;
+ QString m_username;
+ QString m_password;
+ QNetworkRequest m_request;
+ QNetworkReply *m_reply;
+ QString m_responseType;
+ QByteArray m_response;
+ QJSValue m_onload;
+ QJSValue m_onreadystatechange;
+ QJSValue m_ontimeout;
+ QJSValue m_onerror;
+};
+
+#endif // JSKITXMLHTTPREQUEST_P_H
diff --git a/rockworkd/libpebble/jskit/typedarray.js b/rockworkd/libpebble/jskit/typedarray.js
new file mode 100644
index 0000000..d4e00c6
--- /dev/null
+++ b/rockworkd/libpebble/jskit/typedarray.js
@@ -0,0 +1,1037 @@
+/*
+ Copyright (c) 2010, Linden Research, Inc.
+ Copyright (c) 2014, Joshua Bell
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ $/LicenseInfo$
+ */
+
+// Original can be found at:
+// https://bitbucket.org/lindenlab/llsd
+// Modifications by Joshua Bell inexorabletash@gmail.com
+// https://github.com/inexorabletash/polyfill
+
+// ES3/ES5 implementation of the Krhonos Typed Array Specification
+// Ref: http://www.khronos.org/registry/typedarray/specs/latest/
+// Date: 2011-02-01
+//
+// Variations:
+// * Allows typed_array.get/set() as alias for subscripts (typed_array[])
+// * Gradually migrating structure from Khronos spec to ES6 spec
+(function(global) {
+ 'use strict';
+ var undefined = (void 0); // Paranoia
+
+ // Beyond this value, index getters/setters (i.e. array[0], array[1]) are so slow to
+ // create, and consume so much memory, that the browser appears frozen.
+ var MAX_ARRAY_LENGTH = 1e5;
+
+ // Approximations of internal ECMAScript conversion functions
+ function Type(v) {
+ switch(typeof v) {
+ case 'undefined': return 'undefined';
+ case 'boolean': return 'boolean';
+ case 'number': return 'number';
+ case 'string': return 'string';
+ default: return v === null ? 'null' : 'object';
+ }
+ }
+
+ // Class returns internal [[Class]] property, used to avoid cross-frame instanceof issues:
+ function Class(v) { return Object.prototype.toString.call(v).replace(/^\[object *|\]$/g, ''); }
+ function IsCallable(o) { return typeof o === 'function'; }
+ function ToObject(v) {
+ if (v === null || v === undefined) throw TypeError();
+ return Object(v);
+ }
+ function ToInt32(v) { return v >> 0; }
+ function ToUint32(v) { return v >> 0; } //ROCKWORK HACK ALERT: it appears that QT doesn't do the >>> properly, using >> here instead (should be close enough)
+
+ // Snapshot intrinsics
+ var LN2 = Math.LN2,
+ abs = Math.abs,
+ floor = Math.floor,
+ log = Math.log,
+ max = Math.max,
+ min = Math.min,
+ pow = Math.pow,
+ round = Math.round;
+
+ // emulate ES5 getter/setter API using legacy APIs
+ // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx
+ // (second clause tests for Object.defineProperty() in IE<9 that only supports extending DOM prototypes, but
+ // note that IE<9 does not support __defineGetter__ or __defineSetter__ so it just renders the method harmless)
+
+ (function() {
+ var orig = Object.defineProperty;
+ var dom_only = !(function(){try{return Object.defineProperty({},'x',{});}catch(_){return false;}}());
+
+ if (!orig || dom_only) {
+ Object.defineProperty = function (o, prop, desc) {
+ // In IE8 try built-in implementation for defining properties on DOM prototypes.
+ if (orig)
+ try { return orig(o, prop, desc); } catch (_) {}
+ if (o !== Object(o))
+ throw TypeError('Object.defineProperty called on non-object');
+ if (Object.prototype.__defineGetter__ && ('get' in desc))
+ Object.prototype.__defineGetter__.call(o, prop, desc.get);
+ if (Object.prototype.__defineSetter__ && ('set' in desc))
+ Object.prototype.__defineSetter__.call(o, prop, desc.set);
+ if ('value' in desc)
+ o[prop] = desc.value;
+ return o;
+ };
+ }
+ }());
+
+ // ES5: Make obj[index] an alias for obj._getter(index)/obj._setter(index, value)
+ // for index in 0 ... obj.length
+ function makeArrayAccessors(obj) {
+ if (obj.length > MAX_ARRAY_LENGTH) throw RangeError('Array too large for polyfill');
+
+ function makeArrayAccessor(index) {
+ Object.defineProperty(obj, index, {
+ 'get': function() { return obj._getter(index); },
+ 'set': function(v) { obj._setter(index, v); },
+ enumerable: true,
+ configurable: false
+ });
+ }
+
+ var i;
+ for (i = 0; i < obj.length; i += 1) {
+ makeArrayAccessor(i);
+ }
+ }
+
+ // Internal conversion functions:
+ // pack<Type>() - take a number (interpreted as Type), output a byte array
+ // unpack<Type>() - take a byte array, output a Type-like number
+
+ function as_signed(value, bits) { var s = 32 - bits; return (value << s) >> s; }
+ function as_unsigned(value, bits) { var s = 32 - bits; return (value << s) >>> s; }
+
+ function packI8(n) { return [n & 0xff]; }
+ function unpackI8(bytes) { return as_signed(bytes[0], 8); }
+
+ function packU8(n) { return [n & 0xff]; }
+ function unpackU8(bytes) { return as_unsigned(bytes[0], 8); }
+
+ function packU8Clamped(n) { n = round(Number(n)); return [n < 0 ? 0 : n > 0xff ? 0xff : n & 0xff]; }
+
+ function packI16(n) { return [n & 0xff, (n >> 8) & 0xff]; }
+ function unpackI16(bytes) { return as_signed(bytes[1] << 8 | bytes[0], 16); }
+
+ function packU16(n) { return [n & 0xff, (n >> 8) & 0xff]; }
+ function unpackU16(bytes) { return as_unsigned(bytes[1] << 8 | bytes[0], 16); }
+
+ function packI32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; }
+ function unpackI32(bytes) { return as_signed(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); }
+
+ function packU32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; }
+ function unpackU32(bytes) { return as_unsigned(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); }
+
+ function packIEEE754(v, ebits, fbits) {
+
+ var bias = (1 << (ebits - 1)) - 1;
+
+ function roundToEven(n) {
+ var w = floor(n), f = n - w;
+ if (f < 0.5)
+ return w;
+ if (f > 0.5)
+ return w + 1;
+ return w % 2 ? w + 1 : w;
+ }
+
+ // Compute sign, exponent, fraction
+ var s, e, f;
+ if (v !== v) {
+ // NaN
+ // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping
+ e = (1 << ebits) - 1; f = pow(2, fbits - 1); s = 0;
+ } else if (v === Infinity || v === -Infinity) {
+ e = (1 << ebits) - 1; f = 0; s = (v < 0) ? 1 : 0;
+ } else if (v === 0) {
+ e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0;
+ } else {
+ s = v < 0;
+ v = abs(v);
+
+ if (v >= pow(2, 1 - bias)) {
+ // Normalized
+ e = min(floor(log(v) / LN2), 1023);
+ var significand = v / pow(2, e);
+ if (significand < 1) {
+ e -= 1;
+ significand *= 2;
+ }
+ if (significand >= 2) {
+ e += 1;
+ significand /= 2;
+ }
+ var d = pow(2, fbits);
+ f = roundToEven(significand * d) - d;
+ e += bias;
+ if (f / d >= 1) {
+ e += 1;
+ f = 0;
+ }
+ if (e > 2 * bias) {
+ // Overflow
+ e = (1 << ebits) - 1;
+ f = 0;
+ }
+ } else {
+ // Denormalized
+ e = 0;
+ f = roundToEven(v / pow(2, 1 - bias - fbits));
+ }
+ }
+
+ // Pack sign, exponent, fraction
+ var bits = [], i;
+ for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = floor(f / 2); }
+ for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = floor(e / 2); }
+ bits.push(s ? 1 : 0);
+ bits.reverse();
+ var str = bits.join('');
+
+ // Bits to bytes
+ var bytes = [];
+ while (str.length) {
+ bytes.unshift(parseInt(str.substring(0, 8), 2));
+ str = str.substring(8);
+ }
+ return bytes;
+ }
+
+ function unpackIEEE754(bytes, ebits, fbits) {
+ // Bytes to bits
+ var bits = [], i, j, b, str,
+ bias, s, e, f;
+
+ for (i = 0; i < bytes.length; ++i) {
+ b = bytes[i];
+ for (j = 8; j; j -= 1) {
+ bits.push(b % 2 ? 1 : 0); b = b >> 1;
+ }
+ }
+ bits.reverse();
+ str = bits.join('');
+
+ // Unpack sign, exponent, fraction
+ bias = (1 << (ebits - 1)) - 1;
+ s = parseInt(str.substring(0, 1), 2) ? -1 : 1;
+ e = parseInt(str.substring(1, 1 + ebits), 2);
+ f = parseInt(str.substring(1 + ebits), 2);
+
+ // Produce number
+ if (e === (1 << ebits) - 1) {
+ return f !== 0 ? NaN : s * Infinity;
+ } else if (e > 0) {
+ // Normalized
+ return s * pow(2, e - bias) * (1 + f / pow(2, fbits));
+ } else if (f !== 0) {
+ // Denormalized
+ return s * pow(2, -(bias - 1)) * (f / pow(2, fbits));
+ } else {
+ return s < 0 ? -0 : 0;
+ }
+ }
+
+ function unpackF64(b) { return unpackIEEE754(b, 11, 52); }
+ function packF64(v) { return packIEEE754(v, 11, 52); }
+ function unpackF32(b) { return unpackIEEE754(b, 8, 23); }
+ function packF32(v) { return packIEEE754(v, 8, 23); }
+
+ //
+ // 3 The ArrayBuffer Type
+ //
+
+ (function() {
+
+ function ArrayBuffer(length) {
+ length = ToInt32(length);
+ if (length < 0) throw RangeError('ArrayBuffer size is not a small enough positive integer.');
+ Object.defineProperty(this, 'byteLength', {value: length});
+ Object.defineProperty(this, '_bytes', {value: Array(length)});
+
+ for (var i = 0; i < length; i += 1)
+ this._bytes[i] = 0;
+ }
+
+ global.ArrayBuffer = global.ArrayBuffer || ArrayBuffer;
+
+ //
+ // 5 The Typed Array View Types
+ //
+
+ function $TypedArray$() {
+
+ // %TypedArray% ( length )
+ if (!arguments.length || typeof arguments[0] !== 'object') {
+ return (function(length) {
+ length = ToInt32(length);
+ if (length < 0) throw RangeError('length is not a small enough positive integer.');
+ Object.defineProperty(this, 'length', {value: length});
+ Object.defineProperty(this, 'byteLength', {value: length * this.BYTES_PER_ELEMENT});
+ Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(this.byteLength)});
+ Object.defineProperty(this, 'byteOffset', {value: 0});
+
+ }).apply(this, arguments);
+ }
+
+ // %TypedArray% ( typedArray )
+ if (arguments.length >= 1 &&
+ Type(arguments[0]) === 'object' &&
+ arguments[0] instanceof $TypedArray$) {
+ return (function(typedArray){
+ if (this.constructor !== typedArray.constructor) throw TypeError();
+
+ var byteLength = typedArray.length * this.BYTES_PER_ELEMENT;
+ Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)});
+ Object.defineProperty(this, 'byteLength', {value: byteLength});
+ Object.defineProperty(this, 'byteOffset', {value: 0});
+ Object.defineProperty(this, 'length', {value: typedArray.length});
+
+ for (var i = 0; i < this.length; i += 1)
+ this._setter(i, typedArray._getter(i));
+
+ }).apply(this, arguments);
+ }
+
+ // %TypedArray% ( array )
+ if (arguments.length >= 1 &&
+ Type(arguments[0]) === 'object' &&
+ !(arguments[0] instanceof $TypedArray$) &&
+ !(arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) {
+ return (function(array) {
+
+ var byteLength = array.length * this.BYTES_PER_ELEMENT;
+ Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)});
+ Object.defineProperty(this, 'byteLength', {value: byteLength});
+ Object.defineProperty(this, 'byteOffset', {value: 0});
+ Object.defineProperty(this, 'length', {value: array.length});
+
+ for (var i = 0; i < this.length; i += 1) {
+ var s = array[i];
+ this._setter(i, Number(s));
+ }
+ }).apply(this, arguments);
+ }
+
+ // %TypedArray% ( buffer, byteOffset=0, length=undefined )
+ if (arguments.length >= 1 &&
+ Type(arguments[0]) === 'object' &&
+ (arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) {
+ return (function(buffer, byteOffset, length) {
+
+ byteOffset = ToUint32(byteOffset);
+ if (byteOffset > buffer.byteLength)
+ throw RangeError('byteOffset out of range');
+
+ // The given byteOffset must be a multiple of the element
+ // size of the specific type, otherwise an exception is raised.
+ if (byteOffset % this.BYTES_PER_ELEMENT)
+ throw RangeError('buffer length minus the byteOffset is not a multiple of the element size.');
+
+ if (length === undefined) {
+ var byteLength = buffer.byteLength - byteOffset;
+ if (byteLength % this.BYTES_PER_ELEMENT)
+ throw RangeError('length of buffer minus byteOffset not a multiple of the element size');
+ length = byteLength / this.BYTES_PER_ELEMENT;
+
+ } else {
+ length = ToUint32(length);
+ byteLength = length * this.BYTES_PER_ELEMENT;
+ }
+
+ if ((byteOffset + byteLength) > buffer.byteLength)
+ throw RangeError('byteOffset and length reference an area beyond the end of the buffer');
+
+ Object.defineProperty(this, 'buffer', {value: buffer});
+ Object.defineProperty(this, 'byteLength', {value: byteLength});
+ Object.defineProperty(this, 'byteOffset', {value: byteOffset});
+ Object.defineProperty(this, 'length', {value: length});
+
+ }).apply(this, arguments);
+ }
+
+ // %TypedArray% ( all other argument combinations )
+ throw TypeError();
+ }
+
+ // Properties of the %TypedArray Instrinsic Object
+
+ // %TypedArray%.from ( source , mapfn=undefined, thisArg=undefined )
+ Object.defineProperty($TypedArray$, 'from', {value: function(iterable) {
+ return new this(iterable);
+ }});
+
+ // %TypedArray%.of ( ...items )
+ Object.defineProperty($TypedArray$, 'of', {value: function(/*...items*/) {
+ return new this(arguments);
+ }});
+
+ // %TypedArray%.prototype
+ var $TypedArrayPrototype$ = {};
+ $TypedArray$.prototype = $TypedArrayPrototype$;
+
+ // WebIDL: getter type (unsigned long index);
+ Object.defineProperty($TypedArray$.prototype, '_getter', {value: function(index) {
+ if (arguments.length < 1) throw SyntaxError('Not enough arguments');
+
+ index = ToUint32(index);
+ if (index >= this.length)
+ return undefined;
+
+ var bytes = [], i, o;
+ for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT;
+ i < this.BYTES_PER_ELEMENT;
+ i += 1, o += 1) {
+ bytes.push(this.buffer._bytes[o]);
+ }
+ return this._unpack(bytes);
+ }});
+
+ // NONSTANDARD: convenience alias for getter: type get(unsigned long index);
+ Object.defineProperty($TypedArray$.prototype, 'get', {value: $TypedArray$.prototype._getter});
+
+ // WebIDL: setter void (unsigned long index, type value);
+ Object.defineProperty($TypedArray$.prototype, '_setter', {value: function(index, value) {
+ if (arguments.length < 2) throw SyntaxError('Not enough arguments');
+
+ index = ToUint32(index);
+ if (index >= this.length)
+ return;
+
+ var bytes = this._pack(value), i, o;
+ for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT;
+ i < this.BYTES_PER_ELEMENT;
+ i += 1, o += 1) {
+ this.buffer._bytes[o] = bytes[i];
+ }
+ }});
+
+ // get %TypedArray%.prototype.buffer
+ // get %TypedArray%.prototype.byteLength
+ // get %TypedArray%.prototype.byteOffset
+ // -- applied directly to the object in the constructor
+
+ // %TypedArray%.prototype.constructor
+ Object.defineProperty($TypedArray$.prototype, 'constructor', {value: $TypedArray$});
+
+ // %TypedArray%.prototype.copyWithin (target, start, end = this.length )
+ Object.defineProperty($TypedArray$.prototype, 'copyWithin', {value: function(target, start) {
+ var end = arguments[2];
+
+ var o = ToObject(this);
+ var lenVal = o.length;
+ var len = ToUint32(lenVal);
+ len = max(len, 0);
+ var relativeTarget = ToInt32(target);
+ var to;
+ if (relativeTarget < 0)
+ to = max(len + relativeTarget, 0);
+ else
+ to = min(relativeTarget, len);
+ var relativeStart = ToInt32(start);
+ var from;
+ if (relativeStart < 0)
+ from = max(len + relativeStart, 0);
+ else
+ from = min(relativeStart, len);
+ var relativeEnd;
+ if (end === undefined)
+ relativeEnd = len;
+ else
+ relativeEnd = ToInt32(end);
+ var final;
+ if (relativeEnd < 0)
+ final = max(len + relativeEnd, 0);
+ else
+ final = min(relativeEnd, len);
+ var count = min(final - from, len - to);
+ var direction;
+ if (from < to && to < from + count) {
+ direction = -1;
+ from = from + count - 1;
+ to = to + count - 1;
+ } else {
+ direction = 1;
+ }
+ while (count > 0) {
+ o._setter(to, o._getter(from));
+ from = from + direction;
+ to = to + direction;
+ count = count - 1;
+ }
+ return o;
+ }});
+
+ // %TypedArray%.prototype.entries ( )
+ // -- defined in es6.js to shim browsers w/ native TypedArrays
+
+ // %TypedArray%.prototype.every ( callbackfn, thisArg = undefined )
+ Object.defineProperty($TypedArray$.prototype, 'every', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ var thisArg = arguments[1];
+ for (var i = 0; i < len; i++) {
+ if (!callbackfn.call(thisArg, t._getter(i), i, t))
+ return false;
+ }
+ return true;
+ }});
+
+ // %TypedArray%.prototype.fill (value, start = 0, end = this.length )
+ Object.defineProperty($TypedArray$.prototype, 'fill', {value: function(value) {
+ var start = arguments[1],
+ end = arguments[2];
+
+ var o = ToObject(this);
+ var lenVal = o.length;
+ var len = ToUint32(lenVal);
+ len = max(len, 0);
+ var relativeStart = ToInt32(start);
+ var k;
+ if (relativeStart < 0)
+ k = max((len + relativeStart), 0);
+ else
+ k = min(relativeStart, len);
+ var relativeEnd;
+ if (end === undefined)
+ relativeEnd = len;
+ else
+ relativeEnd = ToInt32(end);
+ var final;
+ if (relativeEnd < 0)
+ final = max((len + relativeEnd), 0);
+ else
+ final = min(relativeEnd, len);
+ while (k < final) {
+ o._setter(k, value);
+ k += 1;
+ }
+ return o;
+ }});
+
+ // %TypedArray%.prototype.filter ( callbackfn, thisArg = undefined )
+ Object.defineProperty($TypedArray$.prototype, 'filter', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ var res = [];
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++) {
+ var val = t._getter(i); // in case fun mutates this
+ if (callbackfn.call(thisp, val, i, t))
+ res.push(val);
+ }
+ return new this.constructor(res);
+ }});
+
+ // %TypedArray%.prototype.find (predicate, thisArg = undefined)
+ Object.defineProperty($TypedArray$.prototype, 'find', {value: function(predicate) {
+ var o = ToObject(this);
+ var lenValue = o.length;
+ var len = ToUint32(lenValue);
+ if (!IsCallable(predicate)) throw TypeError();
+ var t = arguments.length > 1 ? arguments[1] : undefined;
+ var k = 0;
+ while (k < len) {
+ var kValue = o._getter(k);
+ var testResult = predicate.call(t, kValue, k, o);
+ if (Boolean(testResult))
+ return kValue;
+ ++k;
+ }
+ return undefined;
+ }});
+
+ // %TypedArray%.prototype.findIndex ( predicate, thisArg = undefined )
+ Object.defineProperty($TypedArray$.prototype, 'findIndex', {value: function(predicate) {
+ var o = ToObject(this);
+ var lenValue = o.length;
+ var len = ToUint32(lenValue);
+ if (!IsCallable(predicate)) throw TypeError();
+ var t = arguments.length > 1 ? arguments[1] : undefined;
+ var k = 0;
+ while (k < len) {
+ var kValue = o._getter(k);
+ var testResult = predicate.call(t, kValue, k, o);
+ if (Boolean(testResult))
+ return k;
+ ++k;
+ }
+ return -1;
+ }});
+
+ // %TypedArray%.prototype.forEach ( callbackfn, thisArg = undefined )
+ Object.defineProperty($TypedArray$.prototype, 'forEach', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ callbackfn.call(thisp, t._getter(i), i, t);
+ }});
+
+ // %TypedArray%.prototype.indexOf (searchElement, fromIndex = 0 )
+ Object.defineProperty($TypedArray$.prototype, 'indexOf', {value: function(searchElement) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (len === 0) return -1;
+ var n = 0;
+ if (arguments.length > 0) {
+ n = Number(arguments[1]);
+ if (n !== n) {
+ n = 0;
+ } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
+ n = (n > 0 || -1) * floor(abs(n));
+ }
+ }
+ if (n >= len) return -1;
+ var k = n >= 0 ? n : max(len - abs(n), 0);
+ for (; k < len; k++) {
+ if (t._getter(k) === searchElement) {
+ return k;
+ }
+ }
+ return -1;
+ }});
+
+ // %TypedArray%.prototype.join ( separator )
+ Object.defineProperty($TypedArray$.prototype, 'join', {value: function(separator) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ var tmp = Array(len);
+ for (var i = 0; i < len; ++i)
+ tmp[i] = t._getter(i);
+ return tmp.join(separator === undefined ? ',' : separator); // Hack for IE7
+ }});
+
+ // %TypedArray%.prototype.keys ( )
+ // -- defined in es6.js to shim browsers w/ native TypedArrays
+
+ // %TypedArray%.prototype.lastIndexOf ( searchElement, fromIndex = this.length-1 )
+ Object.defineProperty($TypedArray$.prototype, 'lastIndexOf', {value: function(searchElement) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (len === 0) return -1;
+ var n = len;
+ if (arguments.length > 1) {
+ n = Number(arguments[1]);
+ if (n !== n) {
+ n = 0;
+ } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
+ n = (n > 0 || -1) * floor(abs(n));
+ }
+ }
+ var k = n >= 0 ? min(n, len - 1) : len - abs(n);
+ for (; k >= 0; k--) {
+ if (t._getter(k) === searchElement)
+ return k;
+ }
+ return -1;
+ }});
+
+ // get %TypedArray%.prototype.length
+ // -- applied directly to the object in the constructor
+
+ // %TypedArray%.prototype.map ( callbackfn, thisArg = undefined )
+ Object.defineProperty($TypedArray$.prototype, 'map', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ var res = []; res.length = len;
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++)
+ res[i] = callbackfn.call(thisp, t._getter(i), i, t);
+ return new this.constructor(res);
+ }});
+
+ // %TypedArray%.prototype.reduce ( callbackfn [, initialValue] )
+ Object.defineProperty($TypedArray$.prototype, 'reduce', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ // no value to return if no initial value and an empty array
+ if (len === 0 && arguments.length === 1) throw TypeError();
+ var k = 0;
+ var accumulator;
+ if (arguments.length >= 2) {
+ accumulator = arguments[1];
+ } else {
+ accumulator = t._getter(k++);
+ }
+ while (k < len) {
+ accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t);
+ k++;
+ }
+ return accumulator;
+ }});
+
+ // %TypedArray%.prototype.reduceRight ( callbackfn [, initialValue] )
+ Object.defineProperty($TypedArray$.prototype, 'reduceRight', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ // no value to return if no initial value, empty array
+ if (len === 0 && arguments.length === 1) throw TypeError();
+ var k = len - 1;
+ var accumulator;
+ if (arguments.length >= 2) {
+ accumulator = arguments[1];
+ } else {
+ accumulator = t._getter(k--);
+ }
+ while (k >= 0) {
+ accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t);
+ k--;
+ }
+ return accumulator;
+ }});
+
+ // %TypedArray%.prototype.reverse ( )
+ Object.defineProperty($TypedArray$.prototype, 'reverse', {value: function() {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ var half = floor(len / 2);
+ for (var i = 0, j = len - 1; i < half; ++i, --j) {
+ var tmp = t._getter(i);
+ t._setter(i, t._getter(j));
+ t._setter(j, tmp);
+ }
+ return t;
+ }});
+
+ // %TypedArray%.prototype.set(array, offset = 0 )
+ // %TypedArray%.prototype.set(typedArray, offset = 0 )
+ // WebIDL: void set(TypedArray array, optional unsigned long offset);
+ // WebIDL: void set(sequence<type> array, optional unsigned long offset);
+ Object.defineProperty($TypedArray$.prototype, 'set', {value: function(index, value) {
+ if (arguments.length < 1) throw SyntaxError('Not enough arguments');
+ var array, sequence, offset, len,
+ i, s, d,
+ byteOffset, byteLength, tmp;
+
+ if (typeof arguments[0] === 'object' && arguments[0].constructor === this.constructor) {
+ // void set(TypedArray array, optional unsigned long offset);
+ array = arguments[0];
+ offset = ToUint32(arguments[1]);
+
+ if (offset + array.length > this.length) {
+ throw RangeError('Offset plus length of array is out of range');
+ }
+
+ byteOffset = this.byteOffset + offset * this.BYTES_PER_ELEMENT;
+ byteLength = array.length * this.BYTES_PER_ELEMENT;
+
+ if (array.buffer === this.buffer) {
+ tmp = [];
+ for (i = 0, s = array.byteOffset; i < byteLength; i += 1, s += 1) {
+ tmp[i] = array.buffer._bytes[s];
+ }
+ for (i = 0, d = byteOffset; i < byteLength; i += 1, d += 1) {
+ this.buffer._bytes[d] = tmp[i];
+ }
+ } else {
+ for (i = 0, s = array.byteOffset, d = byteOffset;
+ i < byteLength; i += 1, s += 1, d += 1) {
+ this.buffer._bytes[d] = array.buffer._bytes[s];
+ }
+ }
+ } else if (typeof arguments[0] === 'object' && typeof arguments[0].length !== 'undefined') {
+ // void set(sequence<type> array, optional unsigned long offset);
+ sequence = arguments[0];
+ len = ToUint32(sequence.length);
+ offset = ToUint32(arguments[1]);
+
+ if (offset + len > this.length) {
+ throw RangeError('Offset plus length of array is out of range');
+ }
+
+ for (i = 0; i < len; i += 1) {
+ s = sequence[i];
+ this._setter(offset + i, Number(s));
+ }
+ } else {
+ throw TypeError('Unexpected argument type(s)');
+ }
+ }});
+
+ // %TypedArray%.prototype.slice ( start, end )
+ Object.defineProperty($TypedArray$.prototype, 'slice', {value: function(start, end) {
+ var o = ToObject(this);
+ var lenVal = o.length;
+ var len = ToUint32(lenVal);
+ var relativeStart = ToInt32(start);
+ var k = (relativeStart < 0) ? max(len + relativeStart, 0) : min(relativeStart, len);
+ var relativeEnd = (end === undefined) ? len : ToInt32(end);
+ var final = (relativeEnd < 0) ? max(len + relativeEnd, 0) : min(relativeEnd, len);
+ var count = final - k;
+ var c = o.constructor;
+ var a = new c(count);
+ var n = 0;
+ while (k < final) {
+ var kValue = o._getter(k);
+ a._setter(n, kValue);
+ ++k;
+ ++n;
+ }
+ return a;
+ }});
+
+ // %TypedArray%.prototype.some ( callbackfn, thisArg = undefined )
+ Object.defineProperty($TypedArray$.prototype, 'some', {value: function(callbackfn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ if (!IsCallable(callbackfn)) throw TypeError();
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++) {
+ if (callbackfn.call(thisp, t._getter(i), i, t)) {
+ return true;
+ }
+ }
+ return false;
+ }});
+
+ // %TypedArray%.prototype.sort ( comparefn )
+ Object.defineProperty($TypedArray$.prototype, 'sort', {value: function(comparefn) {
+ if (this === undefined || this === null) throw TypeError();
+ var t = Object(this);
+ var len = ToUint32(t.length);
+ var tmp = Array(len);
+ for (var i = 0; i < len; ++i)
+ tmp[i] = t._getter(i);
+ if (comparefn) tmp.sort(comparefn); else tmp.sort(); // Hack for IE8/9
+ for (i = 0; i < len; ++i)
+ t._setter(i, tmp[i]);
+ return t;
+ }});
+
+ // %TypedArray%.prototype.subarray(begin = 0, end = this.length )
+ // WebIDL: TypedArray subarray(long begin, optional long end);
+ Object.defineProperty($TypedArray$.prototype, 'subarray', {value: function(start, end) {
+ function clamp(v, min, max) { return v < min ? min : v > max ? max : v; }
+
+ start = ToInt32(start);
+ end = ToInt32(end);
+
+ if (arguments.length < 1) { start = 0; }
+ if (arguments.length < 2) { end = this.length; }
+
+ if (start < 0) { start = this.length + start; }
+ if (end < 0) { end = this.length + end; }
+
+ start = clamp(start, 0, this.length);
+ end = clamp(end, 0, this.length);
+
+ var len = end - start;
+ if (len < 0) {
+ len = 0;
+ }
+
+ return new this.constructor(
+ this.buffer, this.byteOffset + start * this.BYTES_PER_ELEMENT, len);
+ }});
+
+ // %TypedArray%.prototype.toLocaleString ( )
+ // %TypedArray%.prototype.toString ( )
+ // %TypedArray%.prototype.values ( )
+ // %TypedArray%.prototype [ @@iterator ] ( )
+ // get %TypedArray%.prototype [ @@toStringTag ]
+ // -- defined in es6.js to shim browsers w/ native TypedArrays
+
+ function makeTypedArray(elementSize, pack, unpack) {
+ // Each TypedArray type requires a distinct constructor instance with
+ // identical logic, which this produces.
+ var TypedArray = function() {
+ Object.defineProperty(this, 'constructor', {value: TypedArray});
+ $TypedArray$.apply(this, arguments);
+ makeArrayAccessors(this);
+ };
+ if ('__proto__' in TypedArray) {
+ TypedArray.__proto__ = $TypedArray$;
+ } else {
+ TypedArray.from = $TypedArray$.from;
+ TypedArray.of = $TypedArray$.of;
+ }
+
+ TypedArray.BYTES_PER_ELEMENT = elementSize;
+
+ var TypedArrayPrototype = function() {};
+ TypedArrayPrototype.prototype = $TypedArrayPrototype$;
+
+ TypedArray.prototype = new TypedArrayPrototype();
+
+ Object.defineProperty(TypedArray.prototype, 'BYTES_PER_ELEMENT', {value: elementSize});
+ Object.defineProperty(TypedArray.prototype, '_pack', {value: pack});
+ Object.defineProperty(TypedArray.prototype, '_unpack', {value: unpack});
+
+ return TypedArray;
+ }
+
+ var Int8Array = makeTypedArray(1, packI8, unpackI8);
+ var Uint8Array = makeTypedArray(1, packU8, unpackU8);
+ var Uint8ClampedArray = makeTypedArray(1, packU8Clamped, unpackU8);
+ var Int16Array = makeTypedArray(2, packI16, unpackI16);
+ var Uint16Array = makeTypedArray(2, packU16, unpackU16);
+ var Int32Array = makeTypedArray(4, packI32, unpackI32);
+ var Uint32Array = makeTypedArray(4, packU32, unpackU32);
+ var Float32Array = makeTypedArray(4, packF32, unpackF32);
+ var Float64Array = makeTypedArray(8, packF64, unpackF64);
+
+ global.Int8Array = global.Int8Array || Int8Array;
+ global.Uint8Array = global.Uint8Array || Uint8Array;
+ global.Uint8ClampedArray = global.Uint8ClampedArray || Uint8ClampedArray;
+ global.Int16Array = global.Int16Array || Int16Array;
+ global.Uint16Array = global.Uint16Array || Uint16Array;
+ global.Int32Array = global.Int32Array || Int32Array;
+ global.Uint32Array = global.Uint32Array || Uint32Array;
+ global.Float32Array = global.Float32Array || Float32Array;
+ global.Float64Array = global.Float64Array || Float64Array;
+ }());
+
+ //
+ // 6 The DataView View Type
+ //
+
+ (function() {
+ function r(array, index) {
+ return IsCallable(array.get) ? array.get(index) : array[index];
+ }
+
+ var IS_BIG_ENDIAN = (function() {
+ var u16array = new Uint16Array([0x1234]),
+ u8array = new Uint8Array(u16array.buffer);
+ return r(u8array, 0) === 0x12;
+ }());
+
+ // DataView(buffer, byteOffset=0, byteLength=undefined)
+ // WebIDL: Constructor(ArrayBuffer buffer,
+ // optional unsigned long byteOffset,
+ // optional unsigned long byteLength)
+ function DataView(buffer, byteOffset, byteLength) {
+ if (!(buffer instanceof ArrayBuffer || Class(buffer) === 'ArrayBuffer')) throw TypeError();
+
+ byteOffset = ToUint32(byteOffset);
+ if (byteOffset > buffer.byteLength)
+ throw RangeError('byteOffset out of range');
+
+ if (byteLength === undefined)
+ byteLength = buffer.byteLength - byteOffset;
+ else
+ byteLength = ToUint32(byteLength);
+
+ if ((byteOffset + byteLength) > buffer.byteLength)
+ throw RangeError('byteOffset and length reference an area beyond the end of the buffer');
+
+ Object.defineProperty(this, 'buffer', {value: buffer});
+ Object.defineProperty(this, 'byteLength', {value: byteLength});
+ Object.defineProperty(this, 'byteOffset', {value: byteOffset});
+ };
+
+ // get DataView.prototype.buffer
+ // get DataView.prototype.byteLength
+ // get DataView.prototype.byteOffset
+ // -- applied directly to instances by the constructor
+
+ function makeGetter(arrayType) {
+ return function GetViewValue(byteOffset, littleEndian) {
+ byteOffset = ToUint32(byteOffset);
+
+ if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength)
+ throw RangeError('Array index out of range');
+
+ byteOffset += this.byteOffset;
+
+ var uint8Array = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT),
+ bytes = [];
+ for (var i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1)
+ bytes.push(r(uint8Array, i));
+
+ if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN))
+ bytes.reverse();
+
+ return r(new arrayType(new Uint8Array(bytes).buffer), 0);
+ };
+ }
+
+ Object.defineProperty(DataView.prototype, 'getUint8', {value: makeGetter(Uint8Array)});
+ Object.defineProperty(DataView.prototype, 'getInt8', {value: makeGetter(Int8Array)});
+ Object.defineProperty(DataView.prototype, 'getUint16', {value: makeGetter(Uint16Array)});
+ Object.defineProperty(DataView.prototype, 'getInt16', {value: makeGetter(Int16Array)});
+ Object.defineProperty(DataView.prototype, 'getUint32', {value: makeGetter(Uint32Array)});
+ Object.defineProperty(DataView.prototype, 'getInt32', {value: makeGetter(Int32Array)});
+ Object.defineProperty(DataView.prototype, 'getFloat32', {value: makeGetter(Float32Array)});
+ Object.defineProperty(DataView.prototype, 'getFloat64', {value: makeGetter(Float64Array)});
+
+ function makeSetter(arrayType) {
+ return function SetViewValue(byteOffset, value, littleEndian) {
+ byteOffset = ToUint32(byteOffset);
+ if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength)
+ throw RangeError('Array index out of range');
+
+ // Get bytes
+ var typeArray = new arrayType([value]),
+ byteArray = new Uint8Array(typeArray.buffer),
+ bytes = [], i, byteView;
+
+ for (i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1)
+ bytes.push(r(byteArray, i));
+
+ // Flip if necessary
+ if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN))
+ bytes.reverse();
+
+ // Write them
+ byteView = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT);
+ byteView.set(bytes);
+ };
+ }
+
+ Object.defineProperty(DataView.prototype, 'setUint8', {value: makeSetter(Uint8Array)});
+ Object.defineProperty(DataView.prototype, 'setInt8', {value: makeSetter(Int8Array)});
+ Object.defineProperty(DataView.prototype, 'setUint16', {value: makeSetter(Uint16Array)});
+ Object.defineProperty(DataView.prototype, 'setInt16', {value: makeSetter(Int16Array)});
+ Object.defineProperty(DataView.prototype, 'setUint32', {value: makeSetter(Uint32Array)});
+ Object.defineProperty(DataView.prototype, 'setInt32', {value: makeSetter(Int32Array)});
+ Object.defineProperty(DataView.prototype, 'setFloat32', {value: makeSetter(Float32Array)});
+ Object.defineProperty(DataView.prototype, 'setFloat64', {value: makeSetter(Float64Array)});
+
+ global.DataView = global.DataView || DataView;
+
+ }());
+
+}(this));
diff --git a/rockworkd/libpebble/musicendpoint.cpp b/rockworkd/libpebble/musicendpoint.cpp
new file mode 100644
index 0000000..f66afda
--- /dev/null
+++ b/rockworkd/libpebble/musicendpoint.cpp
@@ -0,0 +1,63 @@
+#include "musicendpoint.h"
+#include "pebble.h"
+#include "watchconnection.h"
+
+#include <QDebug>
+
+MusicEndpoint::MusicEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_watchConnection(connection)
+{
+ m_watchConnection->registerEndpointHandler(WatchConnection::EndpointMusicControl, this, "handleMessage");
+}
+
+void MusicEndpoint::setMusicMetadata(const MusicMetaData &metaData)
+{
+ m_metaData = metaData;
+ writeMetadata();
+}
+
+void MusicEndpoint::writeMetadata()
+{
+ if (!m_watchConnection->isConnected()) {
+ return;
+ }
+ QStringList tmp;
+ tmp.append(m_metaData.artist.left(30));
+ tmp.append(m_metaData.album.left(30));
+ tmp.append(m_metaData.title.left(30));
+ QByteArray res = m_watchConnection->buildMessageData(16, tmp); // Not yet sure what the 16 is about :/
+
+ m_watchConnection->writeToPebble(WatchConnection::EndpointMusicControl, res);
+}
+
+void MusicEndpoint::handleMessage(const QByteArray &data)
+{
+ MusicControlButton controlButton;
+ switch (data.toHex().toInt()) {
+ case 0x01:
+ controlButton = MusicControlPlayPause;
+ break;
+ case 0x04:
+ controlButton = MusicControlSkipNext;
+ break;
+ case 0x05:
+ controlButton = MusicControlSkipBack;
+ break;
+ case 0x06:
+ controlButton = MusicControlVolumeUp;
+ break;
+ case 0x07:
+ controlButton = MusicControlVolumeDown;
+ break;
+ case 0x08:
+ writeMetadata();
+ return;
+ default:
+ qWarning() << "Unhandled music control button pressed:" << data.toHex();
+ return;
+ }
+ emit musicControlPressed(controlButton);
+}
+
diff --git a/rockworkd/libpebble/musicendpoint.h b/rockworkd/libpebble/musicendpoint.h
new file mode 100644
index 0000000..1978e46
--- /dev/null
+++ b/rockworkd/libpebble/musicendpoint.h
@@ -0,0 +1,37 @@
+#ifndef MUSICENDPOINT_H
+#define MUSICENDPOINT_H
+
+#include "musicmetadata.h"
+#include "enums.h"
+
+#include <QObject>
+
+class Pebble;
+class WatchConnection;
+
+class MusicEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ explicit MusicEndpoint(Pebble *pebble, WatchConnection *connection);
+
+public slots:
+ void setMusicMetadata(const MusicMetaData &metaData);
+
+private slots:
+ void handleMessage(const QByteArray &data);
+
+signals:
+ void musicControlPressed(MusicControlButton button);
+
+private:
+ void writeMetadata();
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_watchConnection;
+
+ MusicMetaData m_metaData;
+};
+
+#endif // MUSICENDPOINT_H
diff --git a/rockworkd/libpebble/musicmetadata.cpp b/rockworkd/libpebble/musicmetadata.cpp
new file mode 100644
index 0000000..fd40a2a
--- /dev/null
+++ b/rockworkd/libpebble/musicmetadata.cpp
@@ -0,0 +1,14 @@
+#include "musicmetadata.h"
+
+MusicMetaData::MusicMetaData()
+{
+
+}
+
+MusicMetaData::MusicMetaData(const QString &artist, const QString &album, const QString &title):
+ artist(artist),
+ album(album),
+ title(title)
+{
+
+}
diff --git a/rockworkd/libpebble/musicmetadata.h b/rockworkd/libpebble/musicmetadata.h
new file mode 100644
index 0000000..d40872c
--- /dev/null
+++ b/rockworkd/libpebble/musicmetadata.h
@@ -0,0 +1,17 @@
+#ifndef MUSICMETADATA_H
+#define MUSICMETADATA_H
+
+#include <QString>
+
+class MusicMetaData
+{
+public:
+ MusicMetaData();
+ MusicMetaData(const QString &artist, const QString &album, const QString &title);
+
+ QString artist;
+ QString album;
+ QString title;
+};
+
+#endif // MUSICMETADATA_H
diff --git a/rockworkd/libpebble/notification.cpp b/rockworkd/libpebble/notification.cpp
new file mode 100644
index 0000000..4b149b8
--- /dev/null
+++ b/rockworkd/libpebble/notification.cpp
@@ -0,0 +1,79 @@
+#include "notification.h"
+
+Notification::Notification(const QString &sourceId) :
+ m_sourceId(sourceId)
+{
+
+}
+
+QString Notification::sourceId() const
+{
+ return m_sourceId;
+}
+
+void Notification::setSourceId(const QString &sourceId)
+{
+ m_sourceId = sourceId;
+}
+
+QString Notification::sourceName() const
+{
+ return m_sourceName;
+}
+
+void Notification::setSourceName(const QString &sourceName)
+{
+ m_sourceName = sourceName;
+}
+
+QString Notification::sender() const
+{
+ return m_sender;
+}
+
+void Notification::setSender(const QString &sender)
+{
+ m_sender = sender;
+}
+
+QString Notification::subject() const
+{
+ return m_subject;
+}
+
+void Notification::setSubject(const QString &subject)
+{
+ m_subject = subject;
+}
+
+QString Notification::body() const
+{
+ return m_body;
+}
+
+void Notification::setBody(const QString &body)
+{
+ m_body = body;
+}
+
+Notification::NotificationType Notification::type() const
+{
+ return m_type;
+}
+
+void Notification::setType(Notification::NotificationType type)
+{
+ m_type = type;
+}
+
+QString Notification::actToken() const
+{
+ return m_actToken;
+}
+
+void Notification::setActToken(QString actToken)
+{
+ m_actToken = actToken;
+}
+
+
diff --git a/rockworkd/libpebble/notification.h b/rockworkd/libpebble/notification.h
new file mode 100644
index 0000000..1ab76a0
--- /dev/null
+++ b/rockworkd/libpebble/notification.h
@@ -0,0 +1,59 @@
+#ifndef NOTIFICATION_H
+#define NOTIFICATION_H
+
+#include <QString>
+
+class Notification
+{
+public:
+ enum NotificationType {
+ NotificationTypeGeneric,
+ NotificationTypeEmail,
+ NotificationTypeSMS,
+ NotificationTypeFacebook,
+ NotificationTypeTwitter,
+ NotificationTypeTelegram,
+ NotificationTypeWhatsApp,
+ NotificationTypeHangout,
+ NotificationTypeGMail,
+ NotificationTypeWeather,
+ NotificationTypeMusic,
+ NotificationTypeMissedCall,
+ NotificationTypeAlarm,
+ NotificationTypeReminder,
+ };
+
+ Notification(const QString &sourceId = QString());
+
+ QString sourceId() const;
+ void setSourceId(const QString &sourceId);
+
+ QString sourceName() const;
+ void setSourceName(const QString &sourceName);
+
+ QString sender() const;
+ void setSender(const QString &sender);
+
+ QString subject() const;
+ void setSubject(const QString &subject);
+
+ QString body() const;
+ void setBody(const QString &body);
+
+ NotificationType type() const;
+ void setType(NotificationType type);
+
+ QString actToken() const;
+ void setActToken(QString actToken);
+
+private:
+ QString m_sourceId;
+ QString m_sourceName;
+ QString m_sender;
+ QString m_subject;
+ QString m_body;
+ NotificationType m_type = NotificationTypeGeneric;
+ QString m_actToken;
+};
+
+#endif // NOTIFICATION_H
diff --git a/rockworkd/libpebble/notificationendpoint.cpp b/rockworkd/libpebble/notificationendpoint.cpp
new file mode 100644
index 0000000..363563a
--- /dev/null
+++ b/rockworkd/libpebble/notificationendpoint.cpp
@@ -0,0 +1,46 @@
+#include "notificationendpoint.h"
+
+#include "watchconnection.h"
+#include "pebble.h"
+#include "blobdb.h"
+
+#include <QDebug>
+#include <QDateTime>
+
+NotificationEndpoint::NotificationEndpoint(Pebble *pebble, WatchConnection *watchConnection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_watchConnection(watchConnection)
+{
+}
+
+void NotificationEndpoint::sendLegacyNotification(const Notification &notification)
+{
+ LegacyNotification::Source source = LegacyNotification::SourceSMS;
+ switch (notification.type()) {
+ case Notification::NotificationTypeEmail:
+ source = LegacyNotification::SourceEmail;
+ break;
+ case Notification::NotificationTypeFacebook:
+ source = LegacyNotification::SourceFacebook;
+ break;
+ case Notification::NotificationTypeSMS:
+ source = LegacyNotification::SourceSMS;
+ break;
+ case Notification::NotificationTypeTwitter:
+ source = LegacyNotification::SourceTwitter;
+ break;
+ default:
+ source = LegacyNotification::SourceSMS;
+ }
+
+ QString body = notification.subject().isEmpty() ? notification.body() : notification.subject();
+ LegacyNotification legacyNotification(source, notification.sender(), body, QDateTime::currentDateTime(), notification.subject());
+ m_watchConnection->writeToPebble(WatchConnection::EndpointNotification, legacyNotification.serialize());
+}
+
+void NotificationEndpoint::notificationReply(const QByteArray &data)
+{
+ qDebug() << "have notification reply" << data.toHex();
+
+}
diff --git a/rockworkd/libpebble/notificationendpoint.h b/rockworkd/libpebble/notificationendpoint.h
new file mode 100644
index 0000000..211c8cd
--- /dev/null
+++ b/rockworkd/libpebble/notificationendpoint.h
@@ -0,0 +1,64 @@
+#include <QObject>
+#include <QUuid>
+#include <QDateTime>
+
+#include "pebble.h"
+#include "watchconnection.h"
+
+class LegacyNotification: public PebblePacket
+{
+// class Meta:
+// endpoint = 3000
+// endianness = '<'
+public:
+ enum Source {
+ SourceEmail = 0,
+ SourceSMS = 1,
+ SourceFacebook = 2,
+ SourceTwitter = 3
+ };
+
+ LegacyNotification(Source source, const QString &sender, const QString &body, const QDateTime &timestamp, const QString &subject):
+ PebblePacket(),
+ m_source(source),
+ m_sender(sender),
+ m_body(body),
+ m_timestamp(timestamp),
+ m_subject(subject)
+ {}
+
+ QByteArray serialize() const override
+ {
+ QByteArray ret;
+ ret.append((quint8)m_source);
+ ret.append(packString(m_sender));
+ ret.append(packString(m_body));
+ ret.append(packString(QString::number(m_timestamp.toMSecsSinceEpoch())));
+ ret.append(packString(m_subject));
+ return ret;
+ }
+
+private:
+
+ Source m_source; // uint8
+ QString m_sender;
+ QString m_body;
+ QDateTime m_timestamp;
+ QString m_subject;
+};
+
+class NotificationEndpoint: public QObject
+{
+ Q_OBJECT
+public:
+ NotificationEndpoint(Pebble *pebble, WatchConnection *watchConnection);
+
+ void sendLegacyNotification(const Notification &notification);
+
+private slots:
+ void notificationReply(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_watchConnection;
+};
diff --git a/rockworkd/libpebble/pebble.cpp b/rockworkd/libpebble/pebble.cpp
new file mode 100644
index 0000000..5655cc7
--- /dev/null
+++ b/rockworkd/libpebble/pebble.cpp
@@ -0,0 +1,693 @@
+#include "pebble.h"
+#include "watchconnection.h"
+#include "notificationendpoint.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "musicendpoint.h"
+#include "phonecallendpoint.h"
+#include "appmanager.h"
+#include "appmsgmanager.h"
+#include "jskit/jskitmanager.h"
+#include "blobdb.h"
+#include "appdownloader.h"
+#include "screenshotendpoint.h"
+#include "firmwaredownloader.h"
+#include "watchlogendpoint.h"
+#include "core.h"
+#include "platforminterface.h"
+#include "ziphelper.h"
+#include "dataloggingendpoint.h"
+
+#include "QDir"
+#include <QDateTime>
+#include <QStandardPaths>
+#include <QSettings>
+#include <QTimeZone>
+
+Pebble::Pebble(const QBluetoothAddress &address, QObject *parent):
+ QObject(parent),
+ m_address(address)
+{
+ m_storagePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + m_address.toString().replace(':', '_') + "/";
+
+ m_connection = new WatchConnection(this);
+ QObject::connect(m_connection, &WatchConnection::watchConnected, this, &Pebble::onPebbleConnected);
+ QObject::connect(m_connection, &WatchConnection::watchDisconnected, this, &Pebble::onPebbleDisconnected);
+
+ m_connection->registerEndpointHandler(WatchConnection::EndpointVersion, this, "pebbleVersionReceived");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointPhoneVersion, this, "phoneVersionAsked");
+ m_connection->registerEndpointHandler(WatchConnection::EndpointFactorySettings, this, "factorySettingsReceived");
+
+ m_dataLogEndpoint = new DataLoggingEndpoint(this, m_connection);
+
+ m_notificationEndpoint = new NotificationEndpoint(this, m_connection);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::notificationReceived, this, &Pebble::sendNotification);
+
+ m_musicEndpoint = new MusicEndpoint(this, m_connection);
+ m_musicEndpoint->setMusicMetadata(Core::instance()->platform()->musicMetaData());
+ QObject::connect(m_musicEndpoint, &MusicEndpoint::musicControlPressed, Core::instance()->platform(), &PlatformInterface::sendMusicControlCommand);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::musicMetadataChanged, m_musicEndpoint, &MusicEndpoint::setMusicMetadata);
+
+ m_phoneCallEndpoint = new PhoneCallEndpoint(this, m_connection);
+ QObject::connect(m_phoneCallEndpoint, &PhoneCallEndpoint::hangupCall, Core::instance()->platform(), &PlatformInterface::hangupCall);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::incomingCall, m_phoneCallEndpoint, &PhoneCallEndpoint::incomingCall);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::callStarted, m_phoneCallEndpoint, &PhoneCallEndpoint::callStarted);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::callEnded, m_phoneCallEndpoint, &PhoneCallEndpoint::callEnded);
+
+ m_appManager = new AppManager(this, m_connection);
+ QObject::connect(m_appManager, &AppManager::appsChanged, this, &Pebble::installedAppsChanged);
+ QObject::connect(m_appManager, &AppManager::idMismatchDetected, this, &Pebble::resetPebble);
+
+ m_appMsgManager = new AppMsgManager(this, m_appManager, m_connection);
+ m_jskitManager = new JSKitManager(this, m_connection, m_appManager, m_appMsgManager, this);
+ QObject::connect(m_jskitManager, SIGNAL(openURL(const QString&, const QString&)), this, SIGNAL(openURL(const QString&, const QString&)));
+
+ m_blobDB = new BlobDB(this, m_connection);
+ QObject::connect(m_blobDB, &BlobDB::muteSource, this, &Pebble::muteNotificationSource);
+ QObject::connect(m_blobDB, &BlobDB::actionTriggered, Core::instance()->platform(), &PlatformInterface::actionTriggered);
+ QObject::connect(m_blobDB, &BlobDB::appInserted, this, &Pebble::appInstalled);
+ QObject::connect(Core::instance()->platform(), &PlatformInterface::organizerItemsChanged, this, &Pebble::syncCalendar);
+
+ m_appDownloader = new AppDownloader(m_storagePath, this);
+ QObject::connect(m_appDownloader, &AppDownloader::downloadFinished, this, &Pebble::appDownloadFinished);
+
+ m_screenshotEndpoint = new ScreenshotEndpoint(this, m_connection, this);
+ QObject::connect(m_screenshotEndpoint, &ScreenshotEndpoint::screenshotAdded, this, &Pebble::screenshotAdded);
+ QObject::connect(m_screenshotEndpoint, &ScreenshotEndpoint::screenshotRemoved, this, &Pebble::screenshotRemoved);
+
+ m_firmwareDownloader = new FirmwareDownloader(this, m_connection);
+ QObject::connect(m_firmwareDownloader, &FirmwareDownloader::updateAvailableChanged, this, &Pebble::slotUpdateAvailableChanged);
+ QObject::connect(m_firmwareDownloader, &FirmwareDownloader::upgradingChanged, this, &Pebble::upgradingFirmwareChanged);
+
+ m_logEndpoint = new WatchLogEndpoint(this, m_connection);
+ QObject::connect(m_logEndpoint, &WatchLogEndpoint::logsFetched, this, &Pebble::logsDumped);
+
+ QSettings watchInfo(m_storagePath + "/watchinfo.conf", QSettings::IniFormat);
+ m_model = (Model)watchInfo.value("watchModel", (int)ModelUnknown).toInt();
+
+ QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ settings.beginGroup("activityParams");
+ m_healthParams.setEnabled(settings.value("enabled").toBool());
+ m_healthParams.setAge(settings.value("age").toUInt());
+ m_healthParams.setHeight(settings.value("height").toInt());
+ m_healthParams.setGender((HealthParams::Gender)settings.value("gender").toInt());
+ m_healthParams.setWeight(settings.value("weight").toInt());
+ m_healthParams.setMoreActive(settings.value("moreActive").toBool());
+ m_healthParams.setSleepMore(settings.value("sleepMore").toBool());
+ settings.endGroup();
+
+ settings.beginGroup("unitsDistance");
+ m_imperialUnits = settings.value("imperialUnits", false).toBool();
+ settings.endGroup();
+
+ settings.beginGroup("calendar");
+ m_calendarSyncEnabled = settings.value("calendarSyncEnabled", true).toBool();
+ settings.endGroup();
+}
+
+QBluetoothAddress Pebble::address() const
+{
+ return m_address;
+}
+
+QString Pebble::name() const
+{
+ return m_name;
+}
+
+void Pebble::setName(const QString &name)
+{
+ m_name = name;
+}
+
+QBluetoothLocalDevice::Pairing Pebble::pairingStatus() const
+{
+ QBluetoothLocalDevice dev;
+ return dev.pairingStatus(m_address);
+}
+
+bool Pebble::connected() const
+{
+ return m_connection->isConnected() && !m_serialNumber.isEmpty();
+}
+
+void Pebble::connect()
+{
+ qDebug() << "Connecting to Pebble:" << m_name << m_address;
+ m_connection->connectPebble(m_address);
+}
+
+QDateTime Pebble::softwareBuildTime() const
+{
+ return m_softwareBuildTime;
+}
+
+QString Pebble::softwareVersion() const
+{
+ return m_softwareVersion;
+}
+
+QString Pebble::softwareCommitRevision() const
+{
+ return m_softwareCommitRevision;
+}
+
+HardwareRevision Pebble::hardwareRevision() const
+{
+ return m_hardwareRevision;
+}
+
+Model Pebble::model() const
+{
+ return m_model;
+}
+
+void Pebble::setHardwareRevision(HardwareRevision hardwareRevision)
+{
+ m_hardwareRevision = hardwareRevision;
+ switch (m_hardwareRevision) {
+ case HardwareRevisionUNKNOWN:
+ m_hardwarePlatform = HardwarePlatformUnknown;
+ break;
+ case HardwareRevisionTINTIN_EV1:
+ case HardwareRevisionTINTIN_EV2:
+ case HardwareRevisionTINTIN_EV2_3:
+ case HardwareRevisionTINTIN_EV2_4:
+ case HardwareRevisionTINTIN_V1_5:
+ case HardwareRevisionBIANCA:
+ case HardwareRevisionTINTIN_BB:
+ case HardwareRevisionTINTIN_BB2:
+ m_hardwarePlatform = HardwarePlatformAplite;
+ break;
+ case HardwareRevisionSNOWY_EVT2:
+ case HardwareRevisionSNOWY_DVT:
+ case HardwareRevisionBOBBY_SMILES:
+ case HardwareRevisionSNOWY_BB:
+ case HardwareRevisionSNOWY_BB2:
+ m_hardwarePlatform = HardwarePlatformBasalt;
+ break;
+ case HardwareRevisionSPALDING_EVT:
+ case HardwareRevisionSPALDING:
+ case HardwareRevisionSPALDING_BB2:
+ m_hardwarePlatform = HardwarePlatformChalk;
+ break;
+ }
+}
+
+HardwarePlatform Pebble::hardwarePlatform() const
+{
+ return m_hardwarePlatform;
+}
+
+QString Pebble::serialNumber() const
+{
+ return m_serialNumber;
+}
+
+QString Pebble::language() const
+{
+ return m_language;
+}
+
+Capabilities Pebble::capabilities() const
+{
+ return m_capabilities;
+}
+
+bool Pebble::isUnfaithful() const
+{
+ return m_isUnfaithful;
+}
+
+bool Pebble::recovery() const
+{
+ return m_recovery;
+}
+
+bool Pebble::upgradingFirmware() const
+{
+ return m_firmwareDownloader->upgrading();
+}
+
+void Pebble::setHealthParams(const HealthParams &healthParams)
+{
+ m_healthParams = healthParams;
+ m_blobDB->setHealthParams(healthParams);
+ emit healtParamsChanged();
+
+ QSettings healthSettings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ healthSettings.beginGroup("activityParams");
+ healthSettings.setValue("enabled", m_healthParams.enabled());
+ healthSettings.setValue("age", m_healthParams.age());
+ healthSettings.setValue("height", m_healthParams.height());
+ healthSettings.setValue("gender", m_healthParams.gender());
+ healthSettings.setValue("weight", m_healthParams.weight());
+ healthSettings.setValue("moreActive", m_healthParams.moreActive());
+ healthSettings.setValue("sleepMore", m_healthParams.sleepMore());
+
+}
+
+HealthParams Pebble::healthParams() const
+{
+ return m_healthParams;
+}
+
+void Pebble::setImperialUnits(bool imperial)
+{
+ m_imperialUnits = imperial;
+ m_blobDB->setUnits(imperial);
+ emit imperialUnitsChanged();
+
+ QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ settings.beginGroup("unitsDistance");
+ settings.setValue("enabled", m_imperialUnits);
+}
+
+bool Pebble::imperialUnits() const
+{
+ return m_imperialUnits;
+}
+
+void Pebble::dumpLogs(const QString &fileName) const
+{
+ m_logEndpoint->fetchLogs(fileName);
+}
+
+QString Pebble::storagePath() const
+{
+ return m_storagePath;
+}
+
+QHash<QString, bool> Pebble::notificationsFilter() const
+{
+ QHash<QString, bool> ret;
+ QString settingsFile = m_storagePath + "/notifications.conf";
+ QSettings s(settingsFile, QSettings::IniFormat);
+ foreach (const QString &key, s.allKeys()) {
+ ret.insert(key, s.value(key).toBool());
+ }
+ return ret;
+}
+
+void Pebble::setNotificationFilter(const QString &sourceId, bool enabled)
+{
+ QString settingsFile = m_storagePath + "/notifications.conf";
+ QSettings s(settingsFile, QSettings::IniFormat);
+ if (!s.contains(sourceId) || s.value(sourceId).toBool() != enabled) {
+ s.setValue(sourceId, enabled);
+ emit notificationFilterChanged(sourceId, enabled);
+ }
+}
+
+void Pebble::sendNotification(const Notification &notification)
+{
+ if (!notificationsFilter().value(notification.sourceId(), true)) {
+ qDebug() << "Notifications for" << notification.sourceId() << "disabled.";
+ return;
+ }
+ // In case it wasn't there before, make sure to write it to the config now so it will appear in the config app.
+ setNotificationFilter(notification.sourceId(), true);
+
+ qDebug() << "Sending notification from source" << notification.sourceId() << "to watch";
+
+ if (m_softwareVersion < "v3.0") {
+ m_notificationEndpoint->sendLegacyNotification(notification);
+ } else {
+ m_blobDB->insertNotification(notification);
+ }
+}
+
+void Pebble::clearAppDB()
+{
+ m_blobDB->clearApps();
+}
+
+void Pebble::clearTimeline()
+{
+ m_blobDB->clearTimeline();
+}
+
+void Pebble::setCalendarSyncEnabled(bool enabled)
+{
+ if (m_calendarSyncEnabled == enabled) {
+ return;
+ }
+ m_calendarSyncEnabled = enabled;
+ emit calendarSyncEnabledChanged();
+
+ if (!m_calendarSyncEnabled) {
+ m_blobDB->clearTimeline();
+ } else {
+ syncCalendar(Core::instance()->platform()->organizerItems());
+ }
+
+ QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat);
+ settings.beginGroup("calendar");
+ settings.setValue("calendarSyncEnabled", m_calendarSyncEnabled);
+ settings.endGroup();
+}
+
+bool Pebble::calendarSyncEnabled() const
+{
+ return m_calendarSyncEnabled;
+}
+
+void Pebble::syncCalendar(const QList<CalendarEvent> &items)
+{
+ if (connected() && m_calendarSyncEnabled) {
+ m_blobDB->syncCalendar(items);
+ }
+}
+
+void Pebble::installApp(const QString &id)
+{
+ m_appDownloader->downloadApp(id);
+}
+
+void Pebble::sideloadApp(const QString &packageFile)
+{
+ QString targetFile = packageFile;
+ targetFile.remove("file://");
+
+ QString id;
+ int i = 0;
+ do {
+ QDir dir(m_storagePath + "/apps/sideload" + QString::number(i));
+ if (!dir.exists()) {
+ if (!dir.mkpath(dir.absolutePath())) {
+ qWarning() << "Error creating dir for unpacking. Cannot install package" << packageFile;
+ return;
+ }
+ id = "sideload" + QString::number(i);
+ }
+ i++;
+ } while (id.isEmpty());
+
+ if (!ZipHelper::unpackArchive(targetFile, m_storagePath + "/apps/" + id)) {
+ qWarning() << "Error unpacking App zip file" << targetFile << "to" << m_storagePath + "/apps/" + id;
+ return;
+ }
+
+ qDebug() << "Sideload package unpacked.";
+ appDownloadFinished(id);
+}
+
+QList<QUuid> Pebble::installedAppIds()
+{
+ return m_appManager->appUuids();
+}
+
+void Pebble::setAppOrder(const QList<QUuid> &newList)
+{
+ m_appManager->setAppOrder(newList);
+}
+
+AppInfo Pebble::appInfo(const QUuid &uuid)
+{
+ return m_appManager->info(uuid);
+}
+
+void Pebble::removeApp(const QUuid &uuid)
+{
+ qDebug() << "Should remove app:" << uuid;
+ m_blobDB->removeApp(m_appManager->info(uuid));
+ m_appManager->removeApp(uuid);
+}
+
+void Pebble::launchApp(const QUuid &uuid)
+{
+ m_appMsgManager->launchApp(uuid);
+}
+
+void Pebble::requestConfigurationURL(const QUuid &uuid) {
+ if (m_jskitManager->currentApp().uuid() == uuid) {
+ m_jskitManager->showConfiguration();
+ }
+ else {
+ m_jskitManager->setConfigurationId(uuid);
+ m_appMsgManager->launchApp(uuid);
+ }
+}
+
+void Pebble::configurationClosed(const QUuid &uuid, const QString &result)
+{
+ if (m_jskitManager->currentApp().uuid() == uuid) {
+ m_jskitManager->handleWebviewClosed(result);
+ }
+}
+
+void Pebble::requestScreenshot()
+{
+ m_screenshotEndpoint->requestScreenshot();
+}
+
+QStringList Pebble::screenshots() const
+{
+ return m_screenshotEndpoint->screenshots();
+}
+
+void Pebble::removeScreenshot(const QString &filename)
+{
+ m_screenshotEndpoint->removeScreenshot(filename);
+}
+
+bool Pebble::firmwareUpdateAvailable() const
+{
+ return m_firmwareDownloader->updateAvailable();
+}
+
+QString Pebble::candidateFirmwareVersion() const
+{
+ return m_firmwareDownloader->candidateVersion();
+}
+
+QString Pebble::firmwareReleaseNotes() const
+{
+ return m_firmwareDownloader->releaseNotes();
+}
+
+void Pebble::upgradeFirmware() const
+{
+ m_firmwareDownloader->performUpgrade();
+}
+
+void Pebble::onPebbleConnected()
+{
+ qDebug() << "Pebble connected:" << m_name;
+ QByteArray data;
+ WatchDataWriter w(&data);
+ w.write<quint8>(0); // Command fetch
+ QString message = "mfg_color";
+ w.writeLE<quint8>(message.length());
+ w.writeFixedString(message.length(), message);
+ m_connection->writeToPebble(WatchConnection::EndpointFactorySettings, data);
+
+ m_connection->writeToPebble(WatchConnection::EndpointVersion, QByteArray(1, 0));
+}
+
+void Pebble::onPebbleDisconnected()
+{
+ qDebug() << "Pebble disconnected:" << m_name;
+ emit pebbleDisconnected();
+}
+
+void Pebble::pebbleVersionReceived(const QByteArray &data)
+{
+ WatchDataReader wd(data);
+
+ wd.skip(1);
+ m_softwareBuildTime = QDateTime::fromTime_t(wd.read<quint32>());
+ qDebug() << "Software Version build:" << m_softwareBuildTime;
+ m_softwareVersion = wd.readFixedString(32);
+ qDebug() << "Software Version string:" << m_softwareVersion;
+ m_softwareCommitRevision = wd.readFixedString(8);
+ qDebug() << "Software Version commit:" << m_softwareCommitRevision;
+
+ m_recovery = wd.read<quint8>();
+ qDebug() << "Recovery:" << m_recovery;
+ HardwareRevision rev = (HardwareRevision)wd.read<quint8>();
+ setHardwareRevision(rev);
+ qDebug() << "HW Revision:" << rev;
+ qDebug() << "Metadata Version:" << wd.read<quint8>();
+
+ qDebug() << "Safe build:" << QDateTime::fromTime_t(wd.read<quint32>());
+ qDebug() << "Safe version:" << wd.readFixedString(32);
+ qDebug() << "safe commit:" << wd.readFixedString(8);
+ qDebug() << "Safe recovery:" << wd.read<quint8>();
+ qDebug() << "HW Revision:" << wd.read<quint8>();
+ qDebug() << "Metadata Version:" << wd.read<quint8>();
+
+ qDebug() << "BootloaderBuild" << QDateTime::fromTime_t(wd.read<quint32>());
+ qDebug() << "hardwareRevision" << wd.readFixedString(9);
+ m_serialNumber = wd.readFixedString(12);
+ qDebug() << "serialnumber" << m_serialNumber;
+ qDebug() << "BT address" << wd.readBytes(6).toHex();
+ qDebug() << "CRC:" << wd.read<quint32>();
+ qDebug() << "Resource timestamp:" << QDateTime::fromTime_t(wd.read<quint32>());
+ m_language = wd.readFixedString(6);
+ qDebug() << "Language" << m_language;
+ qDebug() << "Language version" << wd.read<quint16>();
+ // Capabilities is 64 bits but QFlags can only do 32 bits. lets split it into 2 * 32.
+ // only 8 bits are used atm anyways.
+ m_capabilities = QFlag(wd.readLE<quint32>());
+ qDebug() << "Capabilities" << QString::number(m_capabilities, 16);
+ qDebug() << "Capabilities" << wd.readLE<quint32>();
+ m_isUnfaithful = wd.read<quint8>();
+ qDebug() << "Is Unfaithful" << m_isUnfaithful;
+
+ // This is useful for debugging
+ //m_isUnfaithful = true;
+
+ if (!m_recovery) {
+ m_appManager->rescan();
+
+ QSettings version(m_storagePath + "/watchinfo.conf", QSettings::IniFormat);
+ if (version.value("syncedWithVersion").toString() != QStringLiteral(VERSION)) {
+ m_isUnfaithful = true;
+ }
+
+ if (m_isUnfaithful) {
+ qDebug() << "Pebble sync state unclear. Resetting Pebble watch.";
+ resetPebble();
+ } else {
+ syncCalendar(Core::instance()->platform()->organizerItems());
+ syncApps();
+ m_blobDB->setHealthParams(m_healthParams);
+ m_blobDB->setUnits(m_imperialUnits);
+ }
+ version.setValue("syncedWithVersion", QStringLiteral(VERSION));
+
+ syncTime();
+ }
+
+ m_firmwareDownloader->checkForNewFirmware();
+ emit pebbleConnected();
+
+}
+
+void Pebble::factorySettingsReceived(const QByteArray &data)
+{
+ qDebug() << "have factory settings" << data.toHex();
+
+ WatchDataReader reader(data);
+ quint8 status = reader.read<quint8>();
+ quint8 len = reader.read<quint8>();
+
+ if (status != 0x01 && len != 0x04) {
+ qWarning() << "Unexpected data reading factory settings";
+ return;
+ }
+ m_model = (Model)reader.read<quint32>();
+ QSettings s(m_storagePath + "/watchinfo.conf", QSettings::IniFormat);
+ s.setValue("watchModel", m_model);
+}
+
+void Pebble::phoneVersionAsked(const QByteArray &data)
+{
+
+ QByteArray res;
+
+ Capabilities sessionCap(CapabilityHealth
+ | CapabilityAppRunState
+ | CapabilityUpdatedMusicProtocol | CapabilityInfiniteLogDumping | Capability8kAppMessages);
+
+ quint32 platformFlags = 16 | 32 | OSAndroid;
+
+ WatchDataWriter writer(&res);
+ writer.writeLE<quint8>(0x01); // ok
+ writer.writeLE<quint32>(0xFFFFFFFF);
+ writer.writeLE<quint32>(sessionCap);
+ writer.write<quint32>(platformFlags);
+ writer.write<quint8>(2); // response version
+ writer.write<quint8>(3); // major version
+ writer.write<quint8>(0); // minor version
+ writer.write<quint8>(0); // bugfix version
+ writer.writeLE<quint64>(sessionCap);
+
+ qDebug() << "sending phone version" << res.toHex();
+
+ m_connection->writeToPebble(WatchConnection::EndpointPhoneVersion, res);
+}
+
+void Pebble::appDownloadFinished(const QString &id)
+{
+ QUuid uuid = m_appManager->scanApp(m_storagePath + "/apps/" + id);
+ if (uuid.isNull()) {
+ qWarning() << "Error scanning downloaded app. Won't install on watch";
+ return;
+ }
+ m_blobDB->insertAppMetaData(m_appManager->info(uuid));
+ m_pendingInstallations.append(uuid);
+}
+
+void Pebble::appInstalled(const QUuid &uuid) {
+ if (m_pendingInstallations.contains(uuid)) {
+ m_appMsgManager->launchApp(uuid);
+ }
+}
+
+void Pebble::muteNotificationSource(const QString &source)
+{
+ setNotificationFilter(source, false);
+}
+
+void Pebble::resetPebble()
+{
+ clearTimeline();
+ syncCalendar(Core::instance()->platform()->organizerItems());
+
+ clearAppDB();
+ syncApps();
+}
+
+void Pebble::syncApps()
+{
+ foreach (const QUuid &appUuid, m_appManager->appUuids()) {
+ if (!m_appManager->info(appUuid).isSystemApp()) {
+ qDebug() << "Inserting app" << m_appManager->info(appUuid).shortName() << "into BlobDB";
+ m_blobDB->insertAppMetaData(m_appManager->info(appUuid));
+ }
+ }
+ // make sure the order is synced too
+ m_appManager->setAppOrder(m_appManager->appUuids());
+}
+
+void Pebble::syncTime()
+{
+ TimeMessage msg(TimeMessage::TimeOperationSetUTC);
+ qDebug() << "Syncing Time" << QDateTime::currentDateTime() << msg.serialize().toHex();
+ m_connection->writeToPebble(WatchConnection::EndpointTime, msg.serialize());
+}
+
+void Pebble::slotUpdateAvailableChanged()
+{
+ qDebug() << "update available" << m_firmwareDownloader->updateAvailable() << m_firmwareDownloader->candidateVersion();
+
+ emit updateAvailableChanged();
+}
+
+
+TimeMessage::TimeMessage(TimeMessage::TimeOperation operation) :
+ m_operation(operation)
+{
+
+}
+QByteArray TimeMessage::serialize() const
+{
+ QByteArray ret;
+ WatchDataWriter writer(&ret);
+ writer.write<quint8>(m_operation);
+ switch (m_operation) {
+ case TimeOperationSetLocaltime:
+ writer.writeLE<quint32>(QDateTime::currentMSecsSinceEpoch() / 1000);
+ break;
+ case TimeOperationSetUTC:
+ writer.write<quint32>(QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000);
+ writer.write<qint16>(QDateTime::currentDateTime().offsetFromUtc() / 60);
+ writer.writePascalString(QDateTime::currentDateTime().timeZone().displayName(QTimeZone::StandardTime));
+ break;
+ default:
+ ;
+ }
+ return ret;
+}
diff --git a/rockworkd/libpebble/pebble.h b/rockworkd/libpebble/pebble.h
new file mode 100644
index 0000000..8650d74
--- /dev/null
+++ b/rockworkd/libpebble/pebble.h
@@ -0,0 +1,225 @@
+#ifndef PEBBLE_H
+#define PEBBLE_H
+
+#include "musicmetadata.h"
+#include "notification.h"
+#include "calendarevent.h"
+#include "appinfo.h"
+#include "healthparams.h"
+
+#include <QObject>
+#include <QBluetoothAddress>
+#include <QBluetoothLocalDevice>
+#include <QTimer>
+
+class WatchConnection;
+class NotificationEndpoint;
+class MusicEndpoint;
+class PhoneCallEndpoint;
+class AppManager;
+class AppMsgManager;
+class BankManager;
+class JSKitManager;
+class BlobDB;
+class AppDownloader;
+class ScreenshotEndpoint;
+class FirmwareDownloader;
+class WatchLogEndpoint;
+class DataLoggingEndpoint;
+
+class Pebble : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(Pebble::NotificationType)
+ Q_PROPERTY(QBluetoothAddress address MEMBER m_address)
+ Q_PROPERTY(QString name MEMBER m_name)
+ Q_PROPERTY(HardwareRevision HardwareRevision READ hardwareRevision)
+ Q_PROPERTY(Model model READ model)
+ Q_PROPERTY(HardwarePlatform hardwarePlatform MEMBER m_hardwarePlatform)
+ Q_PROPERTY(QString softwareVersion MEMBER m_softwareVersion)
+ Q_PROPERTY(QString serialNumber MEMBER m_serialNumber)
+ Q_PROPERTY(QString language MEMBER m_language)
+
+public:
+ explicit Pebble(const QBluetoothAddress &address, QObject *parent = 0);
+
+ QBluetoothAddress address() const;
+
+ QString name() const;
+ void setName(const QString &name);
+
+ QBluetoothLocalDevice::Pairing pairingStatus() const;
+
+ bool connected() const;
+ void connect();
+
+ QDateTime softwareBuildTime() const;
+ QString softwareVersion() const;
+ QString softwareCommitRevision() const;
+ HardwareRevision hardwareRevision() const;
+ Model model() const;
+ HardwarePlatform hardwarePlatform() const;
+ QString serialNumber() const;
+ QString language() const;
+ Capabilities capabilities() const;
+ bool isUnfaithful() const;
+ bool recovery() const;
+
+ QString storagePath() const;
+
+public slots:
+ QHash<QString, bool> notificationsFilter() const;
+ void setNotificationFilter(const QString &sourceId, bool enabled);
+ void sendNotification(const Notification &notification);
+
+ void clearTimeline();
+ void setCalendarSyncEnabled(bool enabled);
+ bool calendarSyncEnabled() const;
+
+ void clearAppDB();
+ void installApp(const QString &id);
+ void sideloadApp(const QString &packageFile);
+ QList<QUuid> installedAppIds();
+ void setAppOrder(const QList<QUuid> &newList);
+ AppInfo appInfo(const QUuid &uuid);
+ void removeApp(const QUuid &uuid);
+
+ void launchApp(const QUuid &uuid);
+
+ void requestConfigurationURL(const QUuid &uuid);
+ void configurationClosed(const QUuid &uuid, const QString &result);
+
+ void requestScreenshot();
+ QStringList screenshots() const;
+ void removeScreenshot(const QString &filename);
+
+ bool firmwareUpdateAvailable() const;
+ QString candidateFirmwareVersion() const;
+ QString firmwareReleaseNotes() const;
+ void upgradeFirmware() const;
+ bool upgradingFirmware() const;
+
+ void setHealthParams(const HealthParams &healthParams);
+ HealthParams healthParams() const;
+
+ void setImperialUnits(bool imperial);
+ bool imperialUnits() const;
+
+ void dumpLogs(const QString &fileName) const;
+
+private slots:
+ void onPebbleConnected();
+ void onPebbleDisconnected();
+ void pebbleVersionReceived(const QByteArray &data);
+ void factorySettingsReceived(const QByteArray &data);
+ void phoneVersionAsked(const QByteArray &data);
+ void appDownloadFinished(const QString &id);
+ void appInstalled(const QUuid &uuid);
+ void muteNotificationSource(const QString &source);
+
+ void resetPebble();
+ void syncApps();
+ void syncTime();
+ void syncCalendar(const QList<CalendarEvent> &items);
+
+ void slotUpdateAvailableChanged();
+
+signals:
+ void pebbleConnected();
+ void pebbleDisconnected();
+ void notificationFilterChanged(const QString &sourceId, bool enabled);
+ void musicControlPressed(MusicControlButton control);
+ void installedAppsChanged();
+ void openURL(const QString &uuid, const QString &url);
+ void screenshotAdded(const QString &filename);
+ void screenshotRemoved(const QString &filename);
+ void updateAvailableChanged();
+ void upgradingFirmwareChanged();
+ void logsDumped(bool success);
+
+ void calendarSyncEnabledChanged();
+ void imperialUnitsChanged();
+ void healtParamsChanged();
+private:
+ void setHardwareRevision(HardwareRevision hardwareRevision);
+
+ QBluetoothAddress m_address;
+ QString m_name;
+ QDateTime m_softwareBuildTime;
+ QString m_softwareVersion;
+ QString m_softwareCommitRevision;
+ HardwareRevision m_hardwareRevision;
+ HardwarePlatform m_hardwarePlatform = HardwarePlatformUnknown;
+ Model m_model = ModelUnknown;
+ QString m_serialNumber;
+ QString m_language;
+ Capabilities m_capabilities = CapabilityNone;
+ bool m_isUnfaithful = false;
+ bool m_recovery = false;
+
+ WatchConnection *m_connection;
+ NotificationEndpoint *m_notificationEndpoint;
+ MusicEndpoint *m_musicEndpoint;
+ PhoneCallEndpoint *m_phoneCallEndpoint;
+ AppManager *m_appManager;
+ AppMsgManager *m_appMsgManager;
+ JSKitManager *m_jskitManager;
+ BankManager *m_bankManager;
+ BlobDB *m_blobDB;
+ AppDownloader *m_appDownloader;
+ ScreenshotEndpoint *m_screenshotEndpoint;
+ FirmwareDownloader *m_firmwareDownloader;
+ WatchLogEndpoint *m_logEndpoint;
+ DataLoggingEndpoint *m_dataLogEndpoint;
+
+ QString m_storagePath;
+ QList<QUuid> m_pendingInstallations;
+
+ bool m_calendarSyncEnabled = true;
+ HealthParams m_healthParams;
+ bool m_imperialUnits = false;
+};
+
+/*
+ Capabilities received from phone:
+ In order, starting at zero, in little-endian (unlike the rest of the messsage), the bits sent by the watch indicate support for:
+ - app run state,
+ - infinite log dumping,
+ - updated music protocol,
+ - extended notification service,
+ - language packs,
+ - 8k app messages,
+ - health,
+ - voice
+
+ The capability bits sent *to* the watch are, starting at zero:
+ - app run state,
+ - infinite log dumping,
+ - updated music service,
+ - extended notification service,
+ - (unused),
+ - 8k app messages,
+ - (unused),
+ - third-party voice
+ */
+
+
+
+class TimeMessage: public PebblePacket
+{
+public:
+ enum TimeOperation {
+ TimeOperationGetRequest = 0x00,
+ TimeOperationGetResponse = 0x01,
+ TimeOperationSetLocaltime = 0x02,
+ TimeOperationSetUTC = 0x03
+ };
+ TimeMessage(TimeOperation operation);
+
+ QByteArray serialize() const override;
+
+private:
+ TimeOperation m_operation = TimeOperationGetRequest;
+};
+
+#endif // PEBBLE_H
diff --git a/rockworkd/libpebble/phonecallendpoint.cpp b/rockworkd/libpebble/phonecallendpoint.cpp
new file mode 100644
index 0000000..afd869d
--- /dev/null
+++ b/rockworkd/libpebble/phonecallendpoint.cpp
@@ -0,0 +1,71 @@
+#include "phonecallendpoint.h"
+
+#include "pebble.h"
+#include "watchconnection.h"
+#include "watchdatareader.h"
+
+PhoneCallEndpoint::PhoneCallEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointPhoneControl, this, "handlePhoneEvent");
+}
+
+void PhoneCallEndpoint::incomingCall(uint cookie, const QString &number, const QString &name)
+{
+ QStringList tmp;
+ tmp.append(number);
+ tmp.append(name);
+
+ char act = CallActionIncoming;
+ // FIXME: Outgoing calls don't seem to work... Maybe something wrong in the enum?
+// if (!incoming) {
+// act = CallActionOutgoing;
+// }
+
+ phoneControl(act, cookie, tmp);
+
+}
+
+void PhoneCallEndpoint::callStarted(uint cookie)
+{
+ phoneControl(CallActionStart, cookie, QStringList());
+}
+
+void PhoneCallEndpoint::callEnded(uint cookie, bool missed)
+{
+ Q_UNUSED(missed)
+ // FIXME: The watch doesn't seem to react on Missed... So let's always "End" it for now
+// phoneControl(missed ? CallActionMissed : CallActionEnd, cookie, QStringList());
+ phoneControl(CallActionEnd, cookie, QStringList());
+}
+
+void PhoneCallEndpoint::phoneControl(char act, uint cookie, QStringList datas)
+{
+ QByteArray head;
+ head.append((char)act);
+ head.append((cookie >> 24)& 0xFF);
+ head.append((cookie >> 16)& 0xFF);
+ head.append((cookie >> 8)& 0xFF);
+ head.append(cookie & 0xFF);
+ if (datas.length()>0) {
+ head.append(m_connection->buildData(datas));
+ }
+
+ m_connection->writeToPebble(WatchConnection::EndpointPhoneControl, head);
+}
+
+void PhoneCallEndpoint::handlePhoneEvent(const QByteArray &data)
+{
+
+ WatchDataReader reader(data);
+ reader.skip(1);
+ uint cookie = reader.read<uint>();
+
+ if (data.at(0) == CallActionHangup) {
+ emit hangupCall(cookie);
+ } else {
+ qWarning() << "received an unhandled phone event" << data.toHex();
+ }
+}
diff --git a/rockworkd/libpebble/phonecallendpoint.h b/rockworkd/libpebble/phonecallendpoint.h
new file mode 100644
index 0000000..994f8a6
--- /dev/null
+++ b/rockworkd/libpebble/phonecallendpoint.h
@@ -0,0 +1,47 @@
+#ifndef PHONECALLENDPOINT_H
+#define PHONECALLENDPOINT_H
+
+#include <QObject>
+
+class Pebble;
+class WatchConnection;
+
+class PhoneCallEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum CallAction{
+ CallActionAnswer = 1,
+ CallActionHangup = 2,
+ CallActionGetState = 3,
+ CallActionIncoming = 4,
+ CallActionOutgoing = 5,
+ CallActionMissed = 6,
+ CallActionRing = 7,
+ CallActionStart = 8,
+ CallActionEnd = 9
+ };
+
+ explicit PhoneCallEndpoint(Pebble *pebble, WatchConnection *connection);
+
+public slots:
+ void incomingCall(uint cookie, const QString &number, const QString &name);
+ void callStarted(uint cookie);
+ void callEnded(uint cookie, bool missed);
+
+signals:
+ void hangupCall(uint cookie);
+
+
+private:
+ void phoneControl(char act, uint cookie, QStringList datas);
+
+private slots:
+ void handlePhoneEvent(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+};
+
+#endif // PHONECALLENDPOINT_H
diff --git a/rockworkd/libpebble/platforminterface.h b/rockworkd/libpebble/platforminterface.h
new file mode 100644
index 0000000..6c67598
--- /dev/null
+++ b/rockworkd/libpebble/platforminterface.h
@@ -0,0 +1,46 @@
+#ifndef PLATFORMINTERFACE_H
+#define PLATFORMINTERFACE_H
+
+#include "libpebble/pebble.h"
+#include "libpebble/musicmetadata.h"
+
+#include <QObject>
+#include <QOrganizerItem>
+
+class PlatformInterface: public QObject
+{
+ Q_OBJECT
+public:
+ PlatformInterface(QObject *parent = 0): QObject(parent) {}
+ virtual ~PlatformInterface() {}
+
+// Notifications
+public:
+ virtual void actionTriggered(const QString &actToken) = 0;
+signals:
+ void notificationReceived(const Notification &notification);
+
+// Music
+public:
+ virtual void sendMusicControlCommand(MusicControlButton controlButton) = 0;
+ virtual MusicMetaData musicMetaData() const = 0;
+signals:
+ void musicMetadataChanged(MusicMetaData metaData);
+
+// Phone calls
+signals:
+ void incomingCall(uint cookie, const QString &number, const QString &name);
+ void callStarted(uint cookie);
+ void callEnded(uint cookie, bool missed);
+public:
+ virtual void hangupCall(uint cookie) = 0;
+
+// Organizer
+public:
+ virtual QList<CalendarEvent> organizerItems() const = 0;
+signals:
+ void organizerItemsChanged(const QList<CalendarEvent> &items);
+
+};
+
+#endif // PLATFORMINTERFACE_H
diff --git a/rockworkd/libpebble/screenshotendpoint.cpp b/rockworkd/libpebble/screenshotendpoint.cpp
new file mode 100644
index 0000000..b31ab70
--- /dev/null
+++ b/rockworkd/libpebble/screenshotendpoint.cpp
@@ -0,0 +1,131 @@
+#include "screenshotendpoint.h"
+
+#include "watchdatawriter.h"
+#include "watchdatareader.h"
+#include "pebble.h"
+
+#include <QImage>
+#include <QDateTime>
+#include <QDir>
+
+ScreenshotEndpoint::ScreenshotEndpoint(Pebble *pebble, WatchConnection *connection, QObject *parent):
+ QObject(parent),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointScreenshot, this, "handleScreenshotData");
+}
+
+void ScreenshotEndpoint::requestScreenshot()
+{
+ ScreenshotRequestPackage package;
+ m_connection->writeToPebble(WatchConnection::EndpointScreenshot, package.serialize());
+}
+
+void ScreenshotEndpoint::removeScreenshot(const QString &filename)
+{
+ QFile f(filename);
+ if (f.exists() && f.remove()) {
+ emit screenshotRemoved(filename);
+ }
+}
+
+QStringList ScreenshotEndpoint::screenshots() const
+{
+ QDir dir(m_pebble->storagePath() + "/screenshots/");
+ QStringList ret;
+ foreach (const QString &filename, dir.entryList(QDir::Files)) {
+ ret << m_pebble->storagePath() + "/screenshots/" + filename;
+ }
+
+ return ret;
+}
+
+void ScreenshotEndpoint::handleScreenshotData(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ int offset = 0;
+
+ if (m_waitingForMore == 0) {
+
+ ResponseCode responseCode = (ResponseCode)reader.read<quint8>();
+ if (responseCode != ResponseCodeOK) {
+ qWarning() << "Error taking screenshot:" << responseCode;
+ return;
+ }
+ m_version = reader.read<quint32>();
+
+ m_width = reader.read<quint32>();
+ m_height = reader.read<quint32>();
+
+ switch (m_version) {
+ case 1:
+ m_waitingForMore = m_width * m_height / 8;
+ break;
+ case 2:
+ m_waitingForMore = m_width * m_height;
+ break;
+ default:
+ qWarning() << "Unsupported screenshot format version";
+ m_waitingForMore = m_width * m_height; // might work :)
+ }
+
+ offset = 13;
+ m_accumulatedData.clear();
+ }
+
+ QByteArray tmp = reader.readBytes(data.length() - offset);
+ m_waitingForMore -= tmp.length();
+ m_accumulatedData.append(tmp);
+
+ if (m_waitingForMore == 0) {
+ QByteArray output;
+ switch (m_version) {
+ case 1: {
+ int rowBytes = m_width / 8;
+ for (quint32 row = 0; row < m_height; row++) {
+ for (quint32 col = 0; col < m_width; col++) {
+ char pixel = (m_accumulatedData.at(row * rowBytes + col / 8) >> (col % 8)) & 1;
+ output.append(pixel * 255);
+ output.append(pixel * 255);
+ output.append(pixel * 255);
+ }
+ }
+ break;
+ }
+ case 2:
+ for (quint32 row = 0; row < m_height; row++) {
+ for (quint32 col = 0; col < m_width; col++) {
+ char pixel = m_accumulatedData.at(row * m_width + col);
+ output.append(((pixel >> 4) & 0b11) * 85);
+ output.append(((pixel >> 2) & 0b11) * 85);
+ output.append(((pixel >> 0) & 0b11) * 85);
+ }
+ }
+ break;
+ default:
+ qWarning() << "Invalid format.";
+ return;
+ }
+
+ QImage image = QImage((uchar*)output.data(), m_width, m_height, QImage::Format_RGB888);
+ QDir dir(m_pebble->storagePath() + "/screenshots/");
+ if (!dir.exists()) {
+ dir.mkpath(dir.absolutePath());
+ }
+ QString filename = dir.absolutePath() + "/" + QDateTime::currentDateTime().toString("yyyyMMddHHmmss") + ".jpg";
+ image.save(filename);
+ qDebug() << "Screenshot saved to" << filename;
+ emit screenshotAdded(filename);
+ }
+}
+
+
+QByteArray ScreenshotRequestPackage::serialize() const
+{
+ QByteArray data;
+ WatchDataWriter writer(&data);
+
+ writer.write<quint8>(m_command);
+ return data;
+}
diff --git a/rockworkd/libpebble/screenshotendpoint.h b/rockworkd/libpebble/screenshotendpoint.h
new file mode 100644
index 0000000..cca6cfd
--- /dev/null
+++ b/rockworkd/libpebble/screenshotendpoint.h
@@ -0,0 +1,52 @@
+#ifndef SCREENSHOTENDPOINT_H
+#define SCREENSHOTENDPOINT_H
+
+#include <QObject>
+
+#include "watchconnection.h"
+class Pebble;
+
+class ScreenshotRequestPackage: public PebblePacket
+{
+public:
+ QByteArray serialize() const override;
+private:
+ quint8 m_command = 0x00;
+};
+
+class ScreenshotEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum ResponseCode {
+ ResponseCodeOK = 0,
+ ResponseCodeMalformedCommand = 1,
+ ResponseCodeOutOfMemory = 2,
+ ResponseCodeAlreadyInProgress = 3
+ };
+
+ explicit ScreenshotEndpoint(Pebble *pebble, WatchConnection *connection, QObject *parent = 0);
+
+ void requestScreenshot();
+ void removeScreenshot(const QString &filename);
+
+ QStringList screenshots() const;
+
+signals:
+ void screenshotAdded(const QString &filename);
+ void screenshotRemoved(const QString &filename);
+
+private slots:
+ void handleScreenshotData(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ quint32 m_waitingForMore = 0;
+ quint32 m_version = 0;
+ quint32 m_width = 0;
+ quint32 m_height = 0;
+ QByteArray m_accumulatedData;
+};
+
+#endif // SCREENSHOTENDPOINT_H
diff --git a/rockworkd/libpebble/timelineitem.cpp b/rockworkd/libpebble/timelineitem.cpp
new file mode 100644
index 0000000..4bc699c
--- /dev/null
+++ b/rockworkd/libpebble/timelineitem.cpp
@@ -0,0 +1,144 @@
+#include "timelineitem.h"
+
+TimelineItem::TimelineItem(TimelineItem::Type type, Flags flags, const QDateTime &timestamp, quint16 duration):
+ TimelineItem(QUuid::createUuid(), type, flags, timestamp, duration)
+{
+
+}
+
+TimelineItem::TimelineItem(const QUuid &uuid, TimelineItem::Type type, Flags flags, const QDateTime &timestamp, quint16 duration):
+ PebblePacket(),
+ m_itemId(uuid),
+ m_timestamp(timestamp),
+ m_duration(duration),
+ m_type(type),
+ m_flags(flags)
+{
+
+}
+
+QUuid TimelineItem::itemId() const
+{
+ return m_itemId;
+}
+
+void TimelineItem::setLayout(quint8 layout)
+{
+ m_layout = layout;
+}
+
+void TimelineItem::setFlags(Flags flags)
+{
+ m_flags = flags;
+}
+
+void TimelineItem::appendAttribute(const TimelineAttribute &attribute)
+{
+ m_attributes.append(attribute);
+}
+
+void TimelineItem::appendAction(const TimelineAction &action)
+{
+ m_actions.append(action);
+}
+
+QList<TimelineAttribute> TimelineItem::attributes() const
+{
+ return m_attributes;
+}
+
+QList<TimelineAction> TimelineItem::actions() const
+{
+ return m_actions;
+}
+
+QByteArray TimelineItem::serialize() const
+{
+ QByteArray ret;
+ ret.append(m_itemId.toRfc4122());
+ ret.append(m_parentId.toRfc4122());
+ int ts = m_timestamp.toMSecsSinceEpoch() / 1000;
+ ret.append(ts & 0xFF); ret.append((ts >> 8) & 0xFF); ret.append((ts >> 16) & 0xFF); ret.append((ts >> 24) & 0xFF);
+ ret.append(m_duration & 0xFF); ret.append(((m_duration >> 8) & 0xFF));
+ ret.append((quint8)m_type);
+ ret.append(m_flags & 0xFF); ret.append(((m_flags >> 8) & 0xFF));
+ ret.append(m_layout);
+
+ QByteArray serializedAttributes;
+ foreach (const TimelineAttribute &attribute, m_attributes) {
+ serializedAttributes.append(attribute.serialize());
+ }
+
+ QByteArray serializedActions;
+ foreach (const TimelineAction &action, m_actions) {
+ serializedActions.append(action.serialize());
+ }
+ quint16 dataLength = serializedAttributes.length() + serializedActions.length();
+ ret.append(dataLength & 0xFF); ret.append(((dataLength >> 8) & 0xFF));
+ ret.append(m_attributes.count());
+ ret.append(m_actions.count());
+ ret.append(serializedAttributes);
+ ret.append(serializedActions);
+ return ret;
+}
+
+TimelineAction::TimelineAction(quint8 actionId, TimelineAction::Type type, const QList<TimelineAttribute> &attributes):
+ PebblePacket(),
+ m_actionId(actionId),
+ m_type(type),
+ m_attributes(attributes)
+{
+
+}
+
+void TimelineAction::appendAttribute(const TimelineAttribute &attribute)
+{
+ m_attributes.append(attribute);
+}
+
+void TimelineAttribute::setContent(const QString &content)
+{
+ m_content = content.toUtf8();
+}
+
+void TimelineAttribute::setContent(TimelineAttribute::IconID iconId)
+{
+ m_content.clear();
+ m_content.append((quint8)iconId);
+ m_content.append('\0');
+ m_content.append('\0');
+ m_content.append(0x80);
+}
+
+void TimelineAttribute::setContent(TimelineAttribute::Color color)
+{
+ m_content.clear();
+ m_content.append((quint8)color);
+}
+
+void TimelineAttribute::setContent(const QStringList &values)
+{
+ m_content.clear();
+ foreach (const QString &value, values) {
+ if (!m_content.isEmpty()) {
+ m_content.append('\0');
+ }
+ m_content.append(value.toUtf8());
+ }
+}
+
+void TimelineAttribute::setContent(quint8 data)
+{
+ m_content.clear();
+ m_content.append(data);
+}
+
+QByteArray TimelineAttribute::serialize() const
+{
+ QByteArray ret;
+ ret.append((quint8)m_type);
+ ret.append(m_content.length() & 0xFF); ret.append(((m_content.length() >> 8) & 0xFF)); // length
+ ret.append(m_content);
+ return ret;
+}
+
diff --git a/rockworkd/libpebble/timelineitem.h b/rockworkd/libpebble/timelineitem.h
new file mode 100644
index 0000000..ed35539
--- /dev/null
+++ b/rockworkd/libpebble/timelineitem.h
@@ -0,0 +1,194 @@
+#ifndef TIMELINEITEM_H
+#define TIMELINEITEM_H
+
+#include <QByteArray>
+#include <QDateTime>
+
+#include "watchconnection.h"
+
+
+class TimelineAttribute
+{
+public:
+ enum Type {
+ TypeTitle = 0x01,
+ TypeSubtitle = 0x02,
+ TypeBody = 0x03,
+ TypeTinyIcon = 0x04,
+ TypeLargeIcon = 0x06,
+ TypeFieldNames = 0x19,
+ TypeFieldValues = 0x1a,
+ TypeColor = 0x1c,
+ TypeRecurring = 0x1f
+ };
+ enum IconID {
+ IconIDDefaultBell = 0x01,
+ IconIDDefaultMissedCall = 0x02,
+ IconIDReminder = 0x03,
+ IconIDFlag = 0x04,
+ IconIDWhatsApp = 0x05,
+ IconIDTwitter = 0x06,
+ IconIDTelegram = 0x07,
+ IconIDHangout = 0x08,
+ IconIDGMail = 0x09,
+ IconIDFlash = 0x0a, // TODO: what service is this?
+ IconIDFacebook = 0x0b,
+ IconIDMusic = 0x0c,
+ IconIDAlarm = 0x0d,
+ IconIDWeather = 0x0e,
+ IconIDGuess = 0x31
+ };
+
+ enum Color {
+ ColorWhite = 0x00,
+ ColorBlack = 0x80,
+ ColorDarkBlue = 0x81,
+ ColorBlue = 0x82,
+ ColorLightBlue = 0x83,
+ ColorDarkGreen = 0x84,
+ ColorGray = 0x85,
+ ColorBlue2 = 0x86,
+ ColorLightBlue2 = 0x87,
+ ColorGreen = 0x88,
+ ColorOliveGreen = 0x89,
+ ColorLightGreen = 0x90,
+ ColorViolet = 0x91,
+ ColorViolet2 = 0x91,
+ ColorBlue3 = 0x92,
+ ColorBrown = 0x93,
+ ColorGray2 = 0x94,
+ ColorBlue4 = 0x95,
+ ColorBlue5 = 0x96,
+ ColorRed = 0xA0,
+ ColorOrange = 0xB8,
+ ColorYellow = 0xBC
+ };
+
+ TimelineAttribute(Type type, const QByteArray &content):
+ m_type(type),
+ m_content(content)
+ {}
+
+ TimelineAttribute(Type type, IconID iconId):
+ m_type(type)
+ {
+ setContent(iconId);
+ }
+ TimelineAttribute(Type type, Color color):
+ m_type(type)
+ {
+ setContent(color);
+ }
+ TimelineAttribute(Type type, const QStringList &values):
+ m_type(type)
+ {
+ setContent(values);
+ }
+ TimelineAttribute(Type type, quint8 data):
+ m_type(type)
+ {
+ setContent(data);
+ }
+
+ void setContent(const QString &content);
+ void setContent(IconID iconId);
+ void setContent(Color color);
+ void setContent(const QStringList &values);
+ void setContent(quint8 data);
+
+ QByteArray serialize() const;
+private:
+ Type m_type;
+ QByteArray m_content;
+};
+
+class TimelineAction: public PebblePacket
+{
+public:
+ enum Type {
+ TypeAncsDismiss = 1,
+ TypeGeneric = 2,
+ TypeResponse = 3,
+ TypeDismiss = 4,
+ TypeHTTP = 5,
+ TypeSnooze = 6,
+ TypeOpenWatchApp = 7,
+ TypeEmpty = 8,
+ TypeRemove = 9,
+ TypeOpenPin = 10
+ };
+ TimelineAction(quint8 actionId, Type type, const QList<TimelineAttribute> &attributes = QList<TimelineAttribute>());
+ void appendAttribute(const TimelineAttribute &attribute);
+
+ QByteArray serialize() const override {
+ QByteArray ret;
+ ret.append(m_actionId);
+ ret.append((quint8)m_type);
+ ret.append(m_attributes.count());
+ foreach (const TimelineAttribute &attr, m_attributes) {
+ ret.append(attr.serialize());
+ }
+ return ret;
+ }
+
+private:
+ quint8 m_actionId;
+ Type m_type;
+ QList<TimelineAttribute> m_attributes;
+};
+
+class TimelineItem: public PebblePacket
+{
+public:
+ enum Type {
+ TypeNotification = 1,
+ TypePin = 2,
+ TypeReminder = 3
+ };
+
+ // TODO: this is probably not complete and maybe even wrong.
+ enum Flag {
+ FlagNone = 0x00,
+ FlagSingleEvent = 0x01,
+ FlagTimeInUTC = 0x02,
+ FlagAllDay = 0x04
+ };
+ Q_DECLARE_FLAGS(Flags, Flag)
+
+ // TODO: This is not complete
+ enum Layout {
+ LayoutGenericPin = 0x01,
+ LayoutCalendar = 0x02
+ };
+
+ TimelineItem(Type type, TimelineItem::Flags flags = FlagNone, const QDateTime &timestamp = QDateTime::currentDateTime(), quint16 duration = 0);
+ TimelineItem(const QUuid &uuid, Type type, Flags flags = FlagNone, const QDateTime &timestamp = QDateTime::currentDateTime(), quint16 duration = 0);
+
+ QUuid itemId() const;
+
+ void setLayout(quint8 layout);
+ void setFlags(Flags flags);
+
+ void appendAttribute(const TimelineAttribute &attribute);
+ void appendAction(const TimelineAction &action);
+
+ QList<TimelineAttribute> attributes() const;
+ QList<TimelineAction> actions() const;
+
+ QByteArray serialize() const override;
+
+private:
+ QUuid m_itemId;
+ QUuid m_parentId;
+ QDateTime m_timestamp;
+ quint16 m_duration = 0;
+ Type m_type;
+ Flags m_flags; // quint16
+ quint8 m_layout = 0x01; // TODO: find out what this is about
+ QList<TimelineAttribute> m_attributes;
+ QList<TimelineAction> m_actions;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineItem::Flags)
+
+#endif // TIMELINEITEM_H
diff --git a/rockworkd/libpebble/uploadmanager.cpp b/rockworkd/libpebble/uploadmanager.cpp
new file mode 100644
index 0000000..6c6860f
--- /dev/null
+++ b/rockworkd/libpebble/uploadmanager.cpp
@@ -0,0 +1,331 @@
+#include "uploadmanager.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+
+static const int CHUNK_SIZE = 2000;
+
+UploadManager::UploadManager(WatchConnection *connection, QObject *parent) :
+ QObject(parent), m_connection(connection),
+ _lastUploadId(0), _state(StateNotStarted)
+{
+ m_connection->registerEndpointHandler(WatchConnection::EndpointPutBytes, this, "handlePutBytesMessage");
+}
+
+uint UploadManager::upload(WatchConnection::UploadType type, int index, quint32 appInstallId, const QString &filename, int size, quint32 crc,
+ SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ qDebug() << "Should enqueue uplodad:" << filename;
+ PendingUpload upload;
+ upload.id = ++_lastUploadId;
+ upload.type = type;
+ upload.index = index;
+ upload.filename = filename;
+ upload.appInstallId = appInstallId;
+ QFile *f = new QFile(filename);
+ if (!f->open(QFile::ReadOnly)) {
+ qWarning() << "Error opening file" << filename << "for reading. Cannot upload file";
+ if (errorCallback) {
+ errorCallback(-1);
+ }
+ }
+ upload.device = f;
+ if (size < 0) {
+ upload.size = f->size();
+ } else {
+ upload.size = size;
+ }
+ upload.remaining = upload.size;
+ upload.crc = crc;
+ upload.successCallback = successCallback;
+ upload.errorCallback = errorCallback;
+ upload.progressCallback = progressCallback;
+
+ if (upload.remaining <= 0) {
+ qWarning() << "upload is empty";
+ if (errorCallback) {
+ errorCallback(-1);
+ return -1;
+ }
+ }
+
+ _pending.enqueue(upload);
+
+ if (_pending.size() == 1) {
+ startNextUpload();
+ }
+
+ return upload.id;
+}
+
+uint UploadManager::uploadAppBinary(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeBinary, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadAppResources(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeResources, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadFile(const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeFile, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadFirmwareBinary(bool recovery, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(recovery ? WatchConnection::UploadTypeRecovery: WatchConnection::UploadTypeFirmware, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadFirmwareResources(const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeSystemResources, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+uint UploadManager::uploadAppWorker(quint32 appInstallId, const QString &filename, quint32 crc, UploadManager::SuccessCallback successCallback, UploadManager::ErrorCallback errorCallback, UploadManager::ProgressCallback progressCallback)
+{
+ return upload(WatchConnection::UploadTypeWorker, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback);
+}
+
+void UploadManager::cancel(uint id, int code)
+{
+ if (_pending.empty()) {
+ qWarning() << "cannot cancel, empty queue";
+ return;
+ }
+
+ if (id == _pending.head().id) {
+ PendingUpload upload = _pending.dequeue();
+ qDebug() << "aborting current upload" << id << "(code:" << code << ")";
+
+ if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) {
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandAbort);
+ writer.write<quint32>(_token);
+
+ qDebug() << "sending abort for upload" << id;
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+ }
+
+ _state = StateNotStarted;
+ _token = 0;
+
+ if (upload.errorCallback) {
+ upload.errorCallback(code);
+ }
+ upload.device->deleteLater();
+
+ if (!_pending.empty()) {
+ startNextUpload();
+ }
+ } else {
+ for (int i = 1; i < _pending.size(); ++i) {
+ if (_pending[i].id == id) {
+ qDebug() << "cancelling upload" << id << "(code:" << code << ")";
+ if (_pending[i].errorCallback) {
+ _pending[i].errorCallback(code);
+ }
+ _pending.at(i).device->deleteLater();
+ _pending.removeAt(i);
+ return;
+ }
+ }
+ qWarning() << "cannot cancel, id" << id << "not found";
+ }
+}
+
+void UploadManager::startNextUpload()
+{
+ Q_ASSERT(!_pending.empty());
+ Q_ASSERT(_state == StateNotStarted);
+
+ PendingUpload &upload = _pending.head();
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandInit);
+ writer.write<quint32>(upload.remaining);
+ if (upload.index != -1) {
+ writer.write<quint8>(upload.type);
+ writer.write<quint8>(upload.index);
+ if (!upload.filename.isEmpty()) {
+ writer.writeCString(upload.filename);
+ }
+ } else {
+ writer.write<quint8>(upload.type|0x80);
+ writer.writeLE<quint32>(upload.appInstallId);
+ }
+
+ qDebug().nospace() << "starting new upload " << upload.id
+ << ", size:" << upload.remaining
+ << ", type:" << upload.type
+ << ", slot:" << upload.index
+ << ", crc:" << upload.crc
+ << ", filename:" << upload.filename;
+
+ qDebug() << msg.toHex();
+
+ _state = StateWaitForToken;
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+}
+
+bool UploadManager::uploadNextChunk(PendingUpload &upload)
+{
+ QByteArray chunk = upload.device->read(qMin<int>(upload.remaining, CHUNK_SIZE));
+
+ if (upload.remaining < CHUNK_SIZE && chunk.size() < upload.remaining) {
+ // Short read!
+ qWarning() << "short read during upload" << upload.id;
+ return false;
+ }
+
+ Q_ASSERT(!chunk.isEmpty());
+ Q_ASSERT(_state = StateInProgress);
+
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandSend);
+ writer.write<quint32>(_token);
+ writer.write<quint32>(chunk.size());
+ msg.append(chunk);
+
+ qDebug() << "sending a chunk of" << chunk.size() << "bytes";
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+
+ upload.remaining -= chunk.size();
+
+ qDebug() << "remaining" << upload.remaining << "/" << upload.size << "bytes";
+
+ return true;
+}
+
+bool UploadManager::commit(PendingUpload &upload)
+{
+ Q_ASSERT(_state == StateCommit);
+ Q_ASSERT(upload.remaining == 0);
+
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandCommit);
+ writer.write<quint32>(_token);
+ writer.write<quint32>(upload.crc);
+
+ qDebug() << "commiting upload" << upload.id;
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+
+ return true;
+}
+
+bool UploadManager::complete(PendingUpload &upload)
+{
+ Q_ASSERT(_state == StateComplete);
+
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(PutBytesCommandComplete);
+ writer.write<quint32>(_token);
+
+ qDebug() << "completing upload" << upload.id;
+
+ m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg);
+
+ return true;
+}
+
+void UploadManager::handlePutBytesMessage(const QByteArray &data)
+{
+ if (_pending.empty()) {
+ qWarning() << "putbytes message, but queue is empty!";
+ return;
+ }
+ Q_ASSERT(!_pending.empty());
+ PendingUpload &upload = _pending.head();
+
+ WatchDataReader reader(data);
+ int status = reader.read<quint8>();
+
+ if (reader.bad() || status != 1) {
+ qWarning() << "upload" << upload.id << "got error code=" << status;
+ cancel(upload.id, status);
+ return;
+ }
+
+ quint32 recv_token = reader.read<quint32>();
+
+ if (reader.bad()) {
+ qWarning() << "upload" << upload.id << ": could not read the token";
+ cancel(upload.id, -1);
+ return;
+ }
+
+ if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) {
+ if (recv_token != _token) {
+ qWarning() << "upload" << upload.id << ": invalid token";
+ cancel(upload.id, -1);
+ return;
+ }
+ }
+
+ switch (_state) {
+ case StateNotStarted:
+ qWarning() << "got packet when upload is not started";
+ break;
+ case StateWaitForToken:
+ qDebug() << "token received";
+ _token = recv_token;
+ _state = StateInProgress;
+
+ /* fallthrough */
+ case StateInProgress:
+ qDebug() << "moving to the next chunk";
+ if (upload.progressCallback) {
+ // Report that the previous chunk has been succesfully uploaded
+ upload.progressCallback(1.0 - (qreal(upload.remaining) / upload.size));
+ }
+ if (upload.remaining > 0) {
+ if (!uploadNextChunk(upload)) {
+ cancel(upload.id, -1);
+ return;
+ }
+ } else {
+ qDebug() << "no additional chunks, commit";
+ _state = StateCommit;
+ if (!commit(upload)) {
+ cancel(upload.id, -1);
+ return;
+ }
+ }
+ break;
+ case StateCommit:
+ qDebug() << "commited succesfully";
+ if (upload.progressCallback) {
+ // Report that all chunks have been succesfully uploaded
+ upload.progressCallback(1.0);
+ }
+ _state = StateComplete;
+ if (!complete(upload)) {
+ cancel(upload.id, -1);
+ return;
+ }
+ break;
+ case StateComplete:
+ qDebug() << "upload" << upload.id << "succesful, invoking callback";
+ if (upload.successCallback) {
+ upload.successCallback();
+ }
+ upload.device->deleteLater();
+ _pending.dequeue();
+ _token = 0;
+ _state = StateNotStarted;
+ if (!_pending.empty()) {
+ startNextUpload();
+ }
+ break;
+ default:
+ qWarning() << "received message in wrong state";
+ break;
+ }
+}
diff --git a/rockworkd/libpebble/uploadmanager.h b/rockworkd/libpebble/uploadmanager.h
new file mode 100644
index 0000000..a717417
--- /dev/null
+++ b/rockworkd/libpebble/uploadmanager.h
@@ -0,0 +1,85 @@
+#ifndef UPLOADMANAGER_H
+#define UPLOADMANAGER_H
+
+#include <functional>
+#include <QQueue>
+#include "watchconnection.h"
+
+class UploadManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit UploadManager(WatchConnection *watch, QObject *parent = 0);
+
+ typedef std::function<void()> SuccessCallback;
+ typedef std::function<void(int)> ErrorCallback;
+ typedef std::function<void(qreal)> ProgressCallback;
+
+ uint upload(WatchConnection::UploadType type, int index, quint32 appInstallId, const QString &filename, int size = -1, quint32 crc = 0,
+ SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ uint uploadAppBinary(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+ uint uploadAppResources(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+ uint uploadAppWorker(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ uint uploadFirmwareBinary(bool recovery, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+ uint uploadFirmwareResources(const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ uint uploadFile(const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback());
+
+ void cancel(uint id, int code = 0);
+
+signals:
+
+private:
+ enum State {
+ StateNotStarted,
+ StateWaitForToken,
+ StateInProgress,
+ StateCommit,
+ StateComplete
+ };
+
+ struct PendingUpload {
+ uint id;
+
+ WatchConnection::UploadType type;
+ int index = -1;
+ QString filename;
+ quint32 appInstallId;
+ QIODevice *device;
+ int size;
+ int remaining;
+ quint32 crc;
+
+ SuccessCallback successCallback;
+ ErrorCallback errorCallback;
+ ProgressCallback progressCallback;
+ };
+
+ enum PutBytesCommand {
+ PutBytesCommandInit = 1,
+ PutBytesCommandSend = 2,
+ PutBytesCommandCommit = 3,
+ PutBytesCommandAbort = 4,
+ PutBytesCommandComplete = 5
+ };
+
+ void startNextUpload();
+ bool uploadNextChunk(PendingUpload &upload);
+ bool commit(PendingUpload &upload);
+ bool complete(PendingUpload &upload);
+
+private slots:
+ void handlePutBytesMessage(const QByteArray &msg);
+
+private:
+ WatchConnection *m_connection;
+ QQueue<PendingUpload> _pending;
+ uint _lastUploadId;
+ State _state;
+ quint32 _token;
+};
+
+#endif // UPLOADMANAGER_H
diff --git a/rockworkd/libpebble/watchconnection.cpp b/rockworkd/libpebble/watchconnection.cpp
new file mode 100644
index 0000000..0778a1d
--- /dev/null
+++ b/rockworkd/libpebble/watchconnection.cpp
@@ -0,0 +1,242 @@
+#include "watchconnection.h"
+#include "watchdatareader.h"
+#include "watchdatawriter.h"
+#include "uploadmanager.h"
+
+#include <QDBusConnection>
+#include <QDBusReply>
+#include <QDebug>
+#include <QBluetoothAddress>
+#include <QBluetoothLocalDevice>
+#include <QBluetoothSocket>
+#include <QtEndian>
+#include <QDateTime>
+
+WatchConnection::WatchConnection(QObject *parent) :
+ QObject(parent),
+ m_socket(nullptr)
+{
+ m_reconnectTimer.setSingleShot(true);
+ QObject::connect(&m_reconnectTimer, &QTimer::timeout, this, &WatchConnection::reconnect);
+
+ m_localDevice = new QBluetoothLocalDevice(this);
+ connect(m_localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, &WatchConnection::hostModeStateChanged);
+
+ m_uploadManager = new UploadManager(this, this);
+}
+
+UploadManager *WatchConnection::uploadManager() const
+{
+ return m_uploadManager;
+}
+
+void WatchConnection::scheduleReconnect()
+{
+ if (m_connectionAttempts == 0) {
+ reconnect();
+ } else if (m_connectionAttempts < 25) {
+ qDebug() << "Attempting to reconnect in 10 seconds";
+ m_reconnectTimer.start(1000 * 10);
+ } else if (m_connectionAttempts < 35) {
+ qDebug() << "Attempting to reconnect in 1 minute";
+ m_reconnectTimer.start(1000 * 60);
+ } else {
+ qDebug() << "Attempting to reconnect in 15 minutes";
+ m_reconnectTimer.start(1000 * 60 * 15);
+ }
+}
+
+void WatchConnection::reconnect()
+{
+ QBluetoothLocalDevice localBtDev;
+ if (localBtDev.pairingStatus(m_pebbleAddress) == QBluetoothLocalDevice::Unpaired) {
+ // Try again in one 10 secs, give the user some time to pair it
+ m_connectionAttempts = 1;
+ scheduleReconnect();
+ return;
+ }
+
+ if (m_socket) {
+ if (m_socket->state() == QBluetoothSocket::ConnectedState) {
+ qDebug() << "Already connected.";
+ return;
+ }
+ delete m_socket;
+ }
+
+ m_socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this);
+ connect(m_socket, &QBluetoothSocket::connected, this, &WatchConnection::pebbleConnected);
+ connect(m_socket, &QBluetoothSocket::readyRead, this, &WatchConnection::readyRead);
+ connect(m_socket, SIGNAL(error(QBluetoothSocket::SocketError)), this, SLOT(socketError(QBluetoothSocket::SocketError)));
+ connect(m_socket, &QBluetoothSocket::disconnected, this, &WatchConnection::pebbleDisconnected);
+ //connect(socket, SIGNAL(bytesWritten(qint64)), SLOT(onBytesWritten(qint64)));
+
+ m_connectionAttempts++;
+
+ // FIXME: Assuming port 1 (with Pebble)
+ m_socket->connectToService(m_pebbleAddress, 1);
+}
+
+void WatchConnection::connectPebble(const QBluetoothAddress &pebble)
+{
+ m_pebbleAddress = pebble;
+ m_connectionAttempts = 0;
+ scheduleReconnect();
+}
+
+bool WatchConnection::isConnected()
+{
+ return m_socket && m_socket->state() == QBluetoothSocket::ConnectedState;
+}
+
+void WatchConnection::writeToPebble(Endpoint endpoint, const QByteArray &data)
+{
+ if (!m_socket || m_socket->state() != QBluetoothSocket::ConnectedState) {
+ qWarning() << "Socket not open. Cannot send data to Pebble. (Endpoint:" << endpoint << ")";
+ return;
+ }
+
+ //qDebug() << "sending message to endpoint" << endpoint;
+ QByteArray msg;
+
+ msg.append((data.length() & 0xFF00) >> 8);
+ msg.append(data.length() & 0xFF);
+
+ msg.append((endpoint & 0xFF00) >> 8);
+ msg.append(endpoint & 0xFF);
+
+ msg.append(data);
+
+ //qDebug() << "Writing:" << msg.toHex();
+ m_socket->write(msg);
+}
+
+void WatchConnection::systemMessage(WatchConnection::SystemMessage msg)
+{
+ QByteArray data;
+ data.append((char)0);
+ data.append((char)msg);
+ writeToPebble(EndpointSystemMessage, data);
+}
+
+bool WatchConnection::registerEndpointHandler(WatchConnection::Endpoint endpoint, QObject *handler, const QString &method)
+{
+ if (m_endpointHandlers.contains(endpoint)) {
+ qWarning() << "Already have a handlder for endpoint" << endpoint;
+ return false;
+ }
+ Callback cb;
+ cb.obj = handler;
+ cb.method = method;
+ m_endpointHandlers.insert(endpoint, cb);
+ return true;
+}
+
+void WatchConnection::pebbleConnected()
+{
+ m_connectionAttempts = 0;
+ emit watchConnected();
+}
+
+void WatchConnection::pebbleDisconnected()
+{
+ qDebug() << "Disconnected";
+ m_socket->close();
+ emit watchDisconnected();
+ if (!m_reconnectTimer.isActive()) {
+ scheduleReconnect();
+ }
+}
+
+void WatchConnection::socketError(QBluetoothSocket::SocketError error)
+{
+ Q_UNUSED(error); // We seem to get UnknownError anyways all the time
+ qDebug() << "SocketError" << error;
+ m_socket->close();
+ emit watchConnectionFailed();
+ if (!m_reconnectTimer.isActive()) {
+ scheduleReconnect();
+ }
+}
+
+void WatchConnection::readyRead()
+{
+// QByteArray data = m_socket->readAll();
+// qDebug() << "data from pebble" << data.toHex();
+
+// QByteArray header = data.left(4);
+// qDebug() << "header:" << header.toHex();
+ if (!m_socket) {
+ return;
+ }
+ int headerLength = 4;
+ uchar header[4];
+ m_socket->peek(reinterpret_cast<char*>(header), headerLength);
+
+ quint16 messageLength = qFromBigEndian<quint16>(&header[0]);
+ Endpoint endpoint = (Endpoint)qFromBigEndian<quint16>(&header[2]);
+
+ if (m_socket->bytesAvailable() < headerLength + messageLength) {
+// qDebug() << "not enough data... waiting for more";
+ return;
+ }
+
+ QByteArray data = m_socket->read(headerLength + messageLength);
+// qDebug() << "Have message for endpoint:" << endpoint << "data:" << data.toHex();
+
+ data = data.right(data.length() - 4);
+
+ if (m_endpointHandlers.contains(endpoint)) {
+ if (m_endpointHandlers.contains(endpoint)) {
+ Callback cb = m_endpointHandlers.value(endpoint);
+ QMetaObject::invokeMethod(cb.obj.data(), cb.method.toLatin1(), Q_ARG(QByteArray, data));
+ }
+ } else {
+ qWarning() << "Have message for unhandled endpoint" << endpoint << data.toHex();
+ }
+
+ if (m_socket->bytesAvailable() > 0) {
+ readyRead();
+ }
+}
+
+void WatchConnection::hostModeStateChanged(QBluetoothLocalDevice::HostMode state)
+{
+ switch (state) {
+ case QBluetoothLocalDevice::HostPoweredOff:
+ qDebug() << "Bluetooth turned off. Stopping any reconnect attempts.";
+ m_reconnectTimer.stop();
+ break;
+ case QBluetoothLocalDevice::HostConnectable:
+ case QBluetoothLocalDevice::HostDiscoverable:
+ case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry:
+ if (m_socket && m_socket->state() != QBluetoothSocket::ConnectedState
+ && m_socket->state() != QBluetoothSocket::ConnectingState
+ && !m_reconnectTimer.isActive()) {
+ qDebug() << "Bluetooth now active. Trying to reconnect";
+ m_connectionAttempts = 0;
+ scheduleReconnect();
+ }
+ }
+}
+
+QByteArray WatchConnection::buildData(QStringList data)
+{
+ QByteArray res;
+ for (QString d : data)
+ {
+ QByteArray tmp = d.left(0xEF).toUtf8();
+ res.append((tmp.length() + 1) & 0xFF);
+ res.append(tmp);
+ res.append('\0');
+ }
+ return res;
+}
+
+QByteArray WatchConnection::buildMessageData(uint lead, QStringList data)
+{
+ QByteArray res;
+ res.append(lead & 0xFF);
+ res.append(buildData(data));
+ return res;
+}
diff --git a/rockworkd/libpebble/watchconnection.h b/rockworkd/libpebble/watchconnection.h
new file mode 100644
index 0000000..f2c3d5f
--- /dev/null
+++ b/rockworkd/libpebble/watchconnection.h
@@ -0,0 +1,154 @@
+#ifndef WATCHCONNECTION_H
+#define WATCHCONNECTION_H
+
+#include <QObject>
+#include <QBluetoothAddress>
+#include <QBluetoothSocket>
+#include <QBluetoothLocalDevice>
+#include <QtEndian>
+#include <QPointer>
+#include <QTimer>
+#include <QFile>
+
+class EndpointHandlerInterface;
+class UploadManager;
+
+class PebblePacket {
+public:
+ PebblePacket() {}
+ virtual ~PebblePacket() = default;
+ virtual QByteArray serialize() const = 0;
+ QByteArray packString(const QString &string) const {
+ QByteArray tmp = string.left(0xEF).toUtf8();
+ QByteArray ret;
+ ret.append((tmp.length() + 1) & 0xFF);
+ ret.append(tmp);
+ ret.append('\0');
+ return ret;
+ }
+};
+
+class Callback
+{
+public:
+ QPointer<QObject> obj;
+ QString method;
+};
+
+class WatchConnection : public QObject
+{
+ Q_OBJECT
+public:
+
+ enum Endpoint {
+ EndpointUnknownEndpoint = 0,
+ EndpointTime = 11,
+ EndpointVersion = 16,
+ EndpointPhoneVersion = 17,
+ EndpointSystemMessage = 18,
+ EndpointMusicControl = 32,
+ EndpointPhoneControl = 33,
+ EndpointApplicationMessage = 48,
+ EndpointLauncher = 49,
+ EndpointAppLaunch = 52,
+ EndpointWatchLogs = 2000,
+// EndpointWatchPing = 2001,
+ EndpointLogDump = 2002,
+// EndpointWatchReset = 2003,
+// EndpointWatchApp = 2004,
+// EndpointAppLogs = 2006,
+ EndpointNotification = 3000,
+// watchEXTENSIBLE_NOTIFS = 3010, // Deprecated in 3.x
+// watchRESOURCE = 4000,
+ EndpointFactorySettings = 5001,
+ EndpointAppManager = 6000, // Deprecated in 3.x
+ EndpointAppFetch = 6001, // New in 3.x
+ EndpointDataLogging = 6778,
+ EndpointScreenshot = 8000,
+// watchFILE_MANAGER = 8181,
+// watchCORE_DUMP = 9000,
+// watchAUDIO = 10000, // New in 3.x
+ EndpointActionHandler = 11440,
+ EndpointBlobDB = 45531, // New in 3.x
+ EndpointSorting = 0xabcd,
+ EndpointPutBytes = 0xbeef
+ };
+
+ enum SystemMessage {
+ SystemMessageFirmwareAvailable = 0,
+ SystemMessageFirmwareStart = 1,
+ SystemMessageFirmwareComplete = 2,
+ SystemMessageFirmwareFail = 3,
+ SystemMessageFirmwareUpToDate = 4,
+ SystemMessageFirmwareOutOfDate = 5,
+ SystemMessageBluetoothStartDiscoverable = 6,
+ SystemMessageBluetoothEndDiscoverable = 7
+ };
+
+ typedef QMap<int, QVariant> Dict;
+ enum DictItemType {
+ DictItemTypeBytes,
+ DictItemTypeString,
+ DictItemTypeUInt,
+ DictItemTypeInt
+ };
+
+ enum UploadType {
+ UploadTypeFirmware = 1,
+ UploadTypeRecovery = 2,
+ UploadTypeSystemResources = 3,
+ UploadTypeResources = 4,
+ UploadTypeBinary = 5,
+ UploadTypeFile = 6,
+ UploadTypeWorker = 7
+ };
+ enum UploadStatus {
+ UploadStatusProgress,
+ UploadStatusFailed,
+ UploadStatusSuccess
+ };
+
+ explicit WatchConnection(QObject *parent = 0);
+ UploadManager *uploadManager() const;
+
+ void connectPebble(const QBluetoothAddress &pebble);
+ bool isConnected();
+
+ QByteArray buildData(QStringList data);
+ QByteArray buildMessageData(uint lead, QStringList data);
+
+ void writeToPebble(Endpoint endpoint, const QByteArray &data);
+ void systemMessage(SystemMessage msg);
+
+ bool registerEndpointHandler(Endpoint endpoint, QObject *handler, const QString &method);
+
+signals:
+ void watchConnected();
+ void watchDisconnected();
+ void watchConnectionFailed();
+
+private:
+ void scheduleReconnect();
+ void reconnect();
+
+private slots:
+ void hostModeStateChanged(QBluetoothLocalDevice::HostMode state);
+ void pebbleConnected();
+ void pebbleDisconnected();
+ void socketError(QBluetoothSocket::SocketError error);
+ void readyRead();
+// void logData(const QByteArray &data);
+
+
+private:
+ QBluetoothAddress m_pebbleAddress;
+ QBluetoothLocalDevice *m_localDevice;
+ QBluetoothSocket *m_socket = nullptr;
+ int m_connectionAttempts = 0;
+ QTimer m_reconnectTimer;
+
+ UploadManager *m_uploadManager;
+ QHash<Endpoint, Callback> m_endpointHandlers;
+};
+
+#endif // WATCHCONNECTION_H
diff --git a/rockworkd/libpebble/watchdatareader.cpp b/rockworkd/libpebble/watchdatareader.cpp
new file mode 100644
index 0000000..0c73c73
--- /dev/null
+++ b/rockworkd/libpebble/watchdatareader.cpp
@@ -0,0 +1,6 @@
+#include "watchdatareader.h"
+
+bool WatchDataReader::bad() const
+{
+ return m_bad;
+}
diff --git a/rockworkd/libpebble/watchdatareader.h b/rockworkd/libpebble/watchdatareader.h
new file mode 100644
index 0000000..58e77d8
--- /dev/null
+++ b/rockworkd/libpebble/watchdatareader.h
@@ -0,0 +1,146 @@
+#ifndef WATCHDATAREADER_H
+#define WATCHDATAREADER_H
+
+#include "watchconnection.h"
+
+#include <QByteArray>
+#include <QtEndian>
+#include <QString>
+#include <QUuid>
+#include <QMap>
+
+class WatchDataReader {
+public:
+ WatchDataReader(const QByteArray &data):
+ m_data(data)
+ {
+ }
+
+ template <typename T>
+ T read() {
+ if (checkBad(sizeof(T))) return 0;
+ const uchar *u = p();
+ m_offset += sizeof(T);
+ return qFromBigEndian<T>(u);
+ }
+
+ inline bool checkBad(int n = 0)
+ {
+ if (m_offset + n > m_data.size()) {
+ m_bad = true;
+ }
+ return m_bad;
+ }
+ inline const uchar * p()
+ {
+ return reinterpret_cast<const uchar *>(&m_data.constData()[m_offset]);
+ }
+ inline void skip(int n)
+ {
+ m_offset += n;
+ checkBad();
+ }
+
+ template <typename T>
+ inline T readLE()
+ {
+ if (checkBad(sizeof(T))) return 0;
+ const uchar *u = p();
+ m_offset += sizeof(T);
+ return qFromLittleEndian<T>(u);
+ }
+ QString readFixedString(int n)
+ {
+ if (checkBad(n)) return QString();
+ const char *u = &m_data.constData()[m_offset];
+ m_offset += n;
+ return QString::fromUtf8(u, strnlen(u, n));
+ }
+ QByteArray peek(int n) {
+ return m_data.left(m_offset + n).right(n);
+ }
+ QUuid readUuid()
+ {
+ if (checkBad(16)) return QString();
+ m_offset += 16;
+ return QUuid::fromRfc4122(m_data.mid(m_offset - 16, 16));
+ }
+ QByteArray readBytes(int n)
+ {
+ if (checkBad(n)) return QByteArray();
+ const char *u = &m_data.constData()[m_offset];
+ m_offset += n;
+ return QByteArray(u, n);
+ }
+ QMap<int, QVariant> readDict()
+ {
+ QMap<int, QVariant> d;
+ if (checkBad(1)) return d;
+
+ const int n = readLE<quint8>();
+
+ for (int i = 0; i < n; i++) {
+ if (checkBad(4 + 1 + 2)) return d;
+ const int key = readLE<qint32>(); // For some reason, this is little endian.
+ const int type = readLE<quint8>();
+ const int width = readLE<quint16>();
+
+ switch (type) {
+ case WatchConnection::DictItemTypeBytes:
+ d.insert(key, QVariant::fromValue(readBytes(width)));
+ break;
+ case WatchConnection::DictItemTypeString:
+ d.insert(key, QVariant::fromValue(readFixedString(width)));
+ break;
+ case WatchConnection::DictItemTypeUInt:
+ switch (width) {
+ case sizeof(quint8):
+ d.insert(key, QVariant::fromValue(readLE<quint8>()));
+ break;
+ case sizeof(quint16):
+ d.insert(key, QVariant::fromValue(readLE<quint16>()));
+ break;
+ case sizeof(quint32):
+ d.insert(key, QVariant::fromValue(readLE<quint32>()));
+ break;
+ default:
+ m_bad = true;
+ return d;
+ }
+
+ break;
+ case WatchConnection::DictItemTypeInt:
+ switch (width) {
+ case sizeof(qint8):
+ d.insert(key, QVariant::fromValue(readLE<qint8>()));
+ break;
+ case sizeof(qint16):
+ d.insert(key, QVariant::fromValue(readLE<qint16>()));
+ break;
+ case sizeof(qint32):
+ d.insert(key, QVariant::fromValue(readLE<qint32>()));
+ break;
+ default:
+ m_bad = true;
+ return d;
+ }
+
+ break;
+ default:
+ m_bad = true;
+ return d;
+ }
+ }
+
+ return d;
+ }
+ bool bad() const;
+
+
+private:
+ QByteArray m_data;
+ int m_offset = 0;
+ bool m_bad = false;
+};
+
+#endif // WATCHDATAREADER_H
diff --git a/rockworkd/libpebble/watchdatawriter.cpp b/rockworkd/libpebble/watchdatawriter.cpp
new file mode 100644
index 0000000..e3caf17
--- /dev/null
+++ b/rockworkd/libpebble/watchdatawriter.cpp
@@ -0,0 +1,144 @@
+#include "watchdatawriter.h"
+#include "watchconnection.h"
+
+void WatchDataWriter::writeBytes(int n, const QByteArray &b)
+{
+ if (b.size() > n) {
+ _buf->append(b.constData(), n);
+ } else {
+ int diff = n - b.size();
+ _buf->append(b);
+ if (diff > 0) {
+ _buf->append(QByteArray(diff, '\0'));
+ }
+ }
+}
+
+void WatchDataWriter::writeFixedString(int n, const QString &s)
+{
+ _buf->append(s.left(n).toUtf8());
+ for (int i = s.left(n).length(); i < n; i++) {
+ _buf->append('\0');
+ }
+}
+
+void WatchDataWriter::writeCString(const QString &s)
+{
+ _buf->append(s.toUtf8());
+ _buf->append('\0');
+}
+
+void WatchDataWriter::writePascalString(const QString &s)
+{
+ _buf->append(s.length());
+ _buf->append(s.toLatin1());
+}
+
+void WatchDataWriter::writeUuid(const QUuid &uuid)
+{
+ writeBytes(16, uuid.toRfc4122());
+}
+
+void WatchDataWriter::writeDict(const QMap<int, QVariant> &d)
+{
+ int size = d.size();
+ if (size > 0xFF) {
+ qWarning() << "Dictionary is too large to encode";
+ writeLE<quint8>(0);
+ return;
+ }
+
+ writeLE<quint8>(size);
+
+ for (QMap<int, QVariant>::const_iterator it = d.constBegin(); it != d.constEnd(); ++it) {
+ writeLE<quint32>(it.key());
+
+ switch (int(it.value().type())) {
+ case QMetaType::Char:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(char));
+ writeLE<char>(it.value().value<char>());
+ break;
+ case QMetaType::Short:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(short));
+ writeLE<short>(it.value().value<short>());
+ break;
+ case QMetaType::Int:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(int));
+ writeLE<int>(it.value().value<int>());
+ break;
+
+ case QMetaType::UChar:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(char));
+ writeLE<char>(it.value().value<char>());
+ break;
+ case QMetaType::UShort:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(short));
+ writeLE<short>(it.value().value<short>());
+ break;
+ case QMetaType::UInt:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(int));
+ writeLE<int>(it.value().value<int>());
+ break;
+
+ case QMetaType::Bool:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(char));
+ writeLE<char>(it.value().value<char>());
+ break;
+
+ case QMetaType::Float: // Treat qreals as ints
+ case QMetaType::Double:
+ writeLE<quint8>(WatchConnection::DictItemTypeInt);
+ writeLE<quint16>(sizeof(int));
+ writeLE<int>(it.value().value<int>());
+ break;
+
+ case QMetaType::QByteArray: {
+ QByteArray ba = it.value().toByteArray();
+ writeLE<quint8>(WatchConnection::DictItemTypeBytes);
+ writeLE<quint16>(ba.size());
+ _buf->append(ba);
+ break;
+ }
+
+ case QMetaType::QVariantList: {
+ // Generally a JS array, which we marshal as a byte array.
+ QVariantList list = it.value().toList();
+ QByteArray ba;
+ ba.reserve(list.size());
+
+ Q_FOREACH (const QVariant &v, list) {
+ ba.append(v.toInt());
+ }
+
+ writeLE<quint8>(WatchConnection::DictItemTypeBytes);
+ writeLE<quint16>(ba.size());
+ _buf->append(ba);
+ break;
+ }
+
+ default:
+ qWarning() << "Unknown dict item type:" << it.value().typeName();
+ /* Fallthrough */
+ case QMetaType::QString:
+ case QMetaType::QUrl:
+ {
+ QByteArray s = it.value().toString().toUtf8();
+ if (s.isEmpty() || s[s.size() - 1] != '\0') {
+ // Add null terminator if it doesn't have one
+ s.append('\0');
+ }
+ writeLE<quint8>(WatchConnection::DictItemTypeString);
+ writeLE<quint16>(s.size());
+ _buf->append(s);
+ break;
+ }
+ }
+ }
+}
diff --git a/rockworkd/libpebble/watchdatawriter.h b/rockworkd/libpebble/watchdatawriter.h
new file mode 100644
index 0000000..8e4adde
--- /dev/null
+++ b/rockworkd/libpebble/watchdatawriter.h
@@ -0,0 +1,69 @@
+#ifndef WATCHDATAWRITER_H
+#define WATCHDATAWRITER_H
+
+#include <QtEndian>
+#include <QByteArray>
+#include <QString>
+#include <QUuid>
+#include <QVariantMap>
+#include <QLoggingCategory>
+
+class WatchDataWriter
+{
+public:
+ WatchDataWriter(QByteArray *buf);
+
+ template <typename T>
+ void write(T v);
+
+ template <typename T>
+ void writeLE(T v);
+
+ void writeBytes(int n, const QByteArray &b);
+
+ void writeFixedString(int n, const QString &s);
+
+ void writeCString(const QString &s);
+
+ void writePascalString(const QString &s);
+
+ void writeUuid(const QUuid &uuid);
+
+ void writeDict(const QMap<int, QVariant> &d);
+
+private:
+ char *p(int n);
+ uchar *up(int n);
+ QByteArray *_buf;
+};
+
+inline WatchDataWriter::WatchDataWriter(QByteArray *buf)
+ : _buf(buf)
+{
+}
+
+template <typename T>
+void WatchDataWriter::write(T v)
+{
+ qToBigEndian(v, up(sizeof(T)));
+}
+
+template <typename T>
+void WatchDataWriter::writeLE(T v)
+{
+ qToLittleEndian(v, up(sizeof(T)));
+}
+
+inline char * WatchDataWriter::p(int n)
+{
+ int size = _buf->size();
+ _buf->resize(size + n);
+ return &_buf->data()[size];
+}
+
+inline uchar * WatchDataWriter::up(int n)
+{
+ return reinterpret_cast<uchar *>(p(n));
+}
+
+#endif
diff --git a/rockworkd/libpebble/watchlogendpoint.cpp b/rockworkd/libpebble/watchlogendpoint.cpp
new file mode 100644
index 0000000..4b6ab26
--- /dev/null
+++ b/rockworkd/libpebble/watchlogendpoint.cpp
@@ -0,0 +1,128 @@
+#include "watchlogendpoint.h"
+#include "watchdatawriter.h"
+#include "watchdatareader.h"
+#include "pebble.h"
+#include "ziphelper.h"
+
+#include <QDir>
+
+WatchLogEndpoint::WatchLogEndpoint(Pebble *pebble, WatchConnection *connection):
+ QObject(pebble),
+ m_pebble(pebble),
+ m_connection(connection)
+{
+ qsrand(QDateTime::currentMSecsSinceEpoch());
+ m_connection->registerEndpointHandler(WatchConnection::EndpointLogDump, this, "logMessageReceived");
+}
+
+void WatchLogEndpoint::fetchLogs(const QString &fileName)
+{
+ if (m_currentEpoch != 0) {
+ qWarning() << "Already dumping logs. Not starting a second time";
+ return;
+ }
+
+ m_currentFile.setFileName(fileName);
+ if (!m_currentFile.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Cannot open log file for writing" << m_currentFile.fileName();
+ emit logsFetched(false);
+ return;
+ }
+
+ fetchForEpoch(m_currentEpoch);
+}
+
+void WatchLogEndpoint::fetchForEpoch(quint8 epoch)
+{
+ qDebug() << "Dumping logs for epoch" << epoch;
+ QString line("=== Generation: %1 ===\n");
+ line = line.arg(epoch);
+ m_currentFile.write(line.toUtf8());
+ RequestLogPacket packet(WatchLogEndpoint::LogCommandRequestLogs, epoch, qrand());
+ m_connection->writeToPebble(WatchConnection::EndpointLogDump, packet.serialize());
+}
+
+void WatchLogEndpoint::logMessageReceived(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ quint8 command = reader.read<quint8>();
+ switch (command) {
+ case LogCommandLogMessage: {
+ LogMessage m(data.right(data.length() - 1));
+ QString line("%1 %2 :%3> %4\n");
+ line = line.arg(m.level()).arg(m.timestamp().toString("yyyy-MM-dd hh:mm:ss")).arg(m.line()).arg(m.message());
+ m_currentFile.write(line.toUtf8());
+ break;
+ }
+ case LogCommandLogMessageDone: {
+ qDebug() << "Log for epoch" << m_currentEpoch << "fetched";
+ m_currentEpoch++;
+ if (m_currentEpoch == 0) {
+ // Depending on the capabilities, there might not be a LogCommandNoLogMessages. Make sure we don't cycle endlessly
+ qDebug() << "All 255 epocs fetched. Stopping";
+ m_currentFile.close();
+ emit logsFetched(true);
+ return;
+ }
+ fetchForEpoch(m_currentEpoch);
+ break;
+ }
+ case LogCommandNoLogMessages:
+ qDebug() << "Log dumping finished";
+ m_currentEpoch = 0;
+ m_currentFile.close();
+ emit logsFetched(true);
+ break;
+ default:
+ qWarning() << "LogEndpoint: Unhandled command" << command;
+ }
+}
+
+RequestLogPacket::RequestLogPacket(WatchLogEndpoint::LogCommand command, quint8 generation, quint32 cookie):
+ m_command(command),
+ m_generation(generation),
+ m_cookie(cookie)
+{
+
+}
+
+QByteArray RequestLogPacket::serialize() const
+{
+ QByteArray msg;
+ WatchDataWriter writer(&msg);
+ writer.write<quint8>(m_command);
+ writer.write<quint8>(m_generation);
+ writer.write<quint32>(m_cookie);
+ return msg;
+}
+
+LogMessage::LogMessage(const QByteArray &data)
+{
+ WatchDataReader reader(data);
+ m_cookie = reader.read<quint32>();
+ m_timestamp = QDateTime::fromTime_t(reader.read<quint32>());
+ int level = reader.read<quint8>();
+ switch (level) {
+ case 0:
+ m_level = '*';
+ break;
+ case 1:
+ m_level = 'E';
+ break;
+ case 50:
+ m_level = 'W';
+ break;
+ case 100:
+ m_level = 'I';
+ break;
+ case 200:
+ m_level = 'D';
+ case 250:
+ m_level = 'V';
+ }
+
+ m_length = reader.read<quint8>();
+ m_line = reader.read<quint16>();
+ m_filename = reader.readFixedString(16);
+ m_message = reader.readFixedString(m_length);
+}
diff --git a/rockworkd/libpebble/watchlogendpoint.h b/rockworkd/libpebble/watchlogendpoint.h
new file mode 100644
index 0000000..4ce58bf
--- /dev/null
+++ b/rockworkd/libpebble/watchlogendpoint.h
@@ -0,0 +1,76 @@
+#ifndef WATCHLOGENDPOINT_H
+#define WATCHLOGENDPOINT_H
+
+#include <QObject>
+#include <QDateTime>
+
+#include "watchconnection.h"
+
+class Pebble;
+
+class LogMessage: public PebblePacket
+{
+public:
+ LogMessage(const QByteArray &data);
+
+ quint32 cookie() const { return m_cookie; }
+ QDateTime timestamp() const { return m_timestamp; }
+ QChar level() const { return m_level; }
+ quint8 length() const { return m_length; }
+ quint16 line() const { return m_line; }
+ QString filename() const { return m_filename; }
+ QString message() const { return m_message; }
+
+ QByteArray serialize() const override { return QByteArray(); }
+private:
+ quint32 m_cookie;
+ QDateTime m_timestamp;
+ QChar m_level;
+ quint8 m_length;
+ quint16 m_line;
+ QString m_filename;
+ QString m_message;
+};
+
+class WatchLogEndpoint : public QObject
+{
+ Q_OBJECT
+public:
+ enum LogCommand {
+ LogCommandRequestLogs = 0x10,
+ LogCommandLogMessage = 0x80,
+ LogCommandLogMessageDone = 0x81,
+ LogCommandNoLogMessages = 0x82
+ };
+
+ explicit WatchLogEndpoint(Pebble *pebble, WatchConnection *connection);
+
+ void fetchLogs(const QString &fileName);
+
+signals:
+ void logsFetched(bool success);
+
+private slots:
+ void fetchForEpoch(quint8 epoch);
+ void logMessageReceived(const QByteArray &data);
+
+private:
+ Pebble *m_pebble;
+ WatchConnection *m_connection;
+ quint8 m_currentEpoch = 0;
+ QFile m_currentFile;
+ QString m_targetArchive;
+};
+
+class RequestLogPacket: public PebblePacket
+{
+public:
+ RequestLogPacket(WatchLogEndpoint::LogCommand command, quint8 generation, quint32 cookie);
+ QByteArray serialize() const;
+private:
+ WatchLogEndpoint::LogCommand m_command;
+ quint8 m_generation;
+ quint32 m_cookie;
+};
+
+#endif // WATCHLOGENDPOINT_H
diff --git a/rockworkd/libpebble/ziphelper.cpp b/rockworkd/libpebble/ziphelper.cpp
new file mode 100644
index 0000000..f18b8aa
--- /dev/null
+++ b/rockworkd/libpebble/ziphelper.cpp
@@ -0,0 +1,91 @@
+#include "ziphelper.h"
+
+#include <QFileInfo>
+#include <QDebug>
+#include <QDir>
+
+#include <quazip/quazipfile.h>
+#include <quazip/quazip.h>
+
+ZipHelper::ZipHelper()
+{
+
+}
+
+bool ZipHelper::unpackArchive(const QString &archiveFilename, const QString &targetDir)
+{
+ QuaZip zipFile(archiveFilename);
+ if (!zipFile.open(QuaZip::mdUnzip)) {
+ qWarning() << "Failed to open zip file" << zipFile.getZipName();
+ return false;
+ }
+
+ foreach (const QuaZipFileInfo &fi, zipFile.getFileInfoList()) {
+ QuaZipFile f(archiveFilename, fi.name);
+ if (!f.open(QFile::ReadOnly)) {
+ qWarning() << "could not extract file" << fi.name;
+ return false;
+ }
+ if (fi.name.endsWith("/")) {
+ qDebug() << "skipping" << fi.name;
+ continue;
+ }
+ qDebug() << "Inflating:" << fi.name;
+ QFileInfo dirInfo(targetDir + "/" + fi.name);
+ if (!dirInfo.absoluteDir().exists() && !dirInfo.absoluteDir().mkpath(dirInfo.absolutePath())) {
+ qWarning() << "Error creating target dir" << dirInfo.absoluteDir();
+ return false;
+ }
+ QFile of(targetDir + "/" + fi.name);
+ if (!of.open(QFile::WriteOnly | QFile::Truncate)) {
+ qWarning() << "Could not open output file for writing" << fi.name;
+ f.close();
+ return false;
+ }
+ of.write(f.readAll());
+ f.close();
+ of.close();
+ }
+ return true;
+}
+
+bool ZipHelper::packArchive(const QString &archiveFilename, const QString &sourceDir)
+{
+ QuaZip zip(archiveFilename);
+ if (!zip.open(QuaZip::mdCreate)){
+ qWarning() << "Error creating zip file";
+ return false;
+ }
+
+ QDir dir(sourceDir);
+ QuaZipFile outfile(&zip);
+
+ foreach (const QFileInfo &fi, dir.entryInfoList()) {
+ if (!fi.isFile()) {
+ continue;
+ }
+ qDebug() << "have file" << fi.absoluteFilePath();
+ QuaZipNewInfo newInfo(fi.fileName(), fi.absoluteFilePath());
+
+ if (!outfile.open(QFile::WriteOnly, newInfo)) {
+ qWarning() << "Error opening zipfile for writing";
+ zip.close();
+ return false;
+ }
+
+ QFile sourceFile(fi.absoluteFilePath());
+ if (!sourceFile.open(QFile::ReadOnly)) {
+ qWarning() << "Error opening log file for reading" << fi.absoluteFilePath();
+ outfile.close();
+ zip.close();
+ return false;
+ }
+ outfile.write(sourceFile.readAll());
+ outfile.close();
+ sourceFile.close();
+
+ }
+ outfile.close();
+ zip.close();
+ return true;
+}
diff --git a/rockworkd/libpebble/ziphelper.h b/rockworkd/libpebble/ziphelper.h
new file mode 100644
index 0000000..fe3a7a1
--- /dev/null
+++ b/rockworkd/libpebble/ziphelper.h
@@ -0,0 +1,15 @@
+#ifndef ZIPHELPER_H
+#define ZIPHELPER_H
+
+#include <QString>
+
+class ZipHelper
+{
+public:
+ ZipHelper();
+
+ static bool unpackArchive(const QString &archiveFilename, const QString &targetDir);
+ static bool packArchive(const QString &archiveFilename, const QString &sourceDir);
+};
+
+#endif // ZIPHELPER_H
diff --git a/rockworkd/main.cpp b/rockworkd/main.cpp
new file mode 100644
index 0000000..7c58c12
--- /dev/null
+++ b/rockworkd/main.cpp
@@ -0,0 +1,22 @@
+#include <QCoreApplication>
+
+#include "core.h"
+
+#ifdef ENABLE_TESTING
+#include <QGuiApplication>
+#endif
+
+int main(int argc, char *argv[])
+{
+
+#ifdef ENABLE_TESTING
+ QGuiApplication a(argc, argv);
+#else
+ QCoreApplication a(argc, argv);
+#endif
+
+ Core::instance()->init();
+
+ return a.exec();
+}
+
diff --git a/rockworkd/pebblemanager.cpp b/rockworkd/pebblemanager.cpp
new file mode 100644
index 0000000..126000e
--- /dev/null
+++ b/rockworkd/pebblemanager.cpp
@@ -0,0 +1,95 @@
+#include "pebblemanager.h"
+
+#include "core.h"
+
+#include "libpebble/platforminterface.h"
+
+#include <QHash>
+
+#ifdef ENABLE_TESTING
+#include <QQuickView>
+#include <QQmlEngine>
+#include <QQmlContext>
+#endif
+
+PebbleManager::PebbleManager(QObject *parent) : QObject(parent)
+{
+ m_bluezClient = new BluezClient(this);
+ connect(m_bluezClient, &BluezClient::devicesChanged, this, &PebbleManager::loadPebbles);
+ loadPebbles();
+}
+
+QList<Pebble *> PebbleManager::pebbles() const
+{
+ return m_pebbles;
+}
+
+void PebbleManager::loadPebbles()
+{
+ QList<Device> pairedPebbles = m_bluezClient->pairedPebbles();
+ foreach (const Device &device, pairedPebbles) {
+ qDebug() << "loading pebble" << device.address.toString();
+ Pebble *pebble = get(device.address);
+ if (!pebble) {
+ qDebug() << "creating new pebble";
+ pebble = new Pebble(device.address, this);
+ pebble->setName(device.name);
+ setupPebble(pebble);
+ m_pebbles.append(pebble);
+ qDebug() << "have pebbles:" << m_pebbles.count() << this;
+ emit pebbleAdded(pebble);
+ }
+ if (!pebble->connected()) {
+ pebble->connect();
+ }
+ }
+ QList<Pebble*> pebblesToRemove;
+ foreach (Pebble *pebble, m_pebbles) {
+ bool found = false;
+ foreach (const Device &dev, pairedPebbles) {
+ if (dev.address == pebble->address()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ pebblesToRemove << pebble;
+ }
+ }
+
+ while (!pebblesToRemove.isEmpty()) {
+ Pebble *pebble = pebblesToRemove.takeFirst();
+ qDebug() << "Removing pebble" << pebble->address();
+ m_pebbles.removeOne(pebble);
+ emit pebbleRemoved(pebble);
+ pebble->deleteLater();
+ }
+}
+
+void PebbleManager::pebbleConnected()
+{
+}
+
+void PebbleManager::setupPebble(Pebble *pebble)
+{
+
+#ifdef ENABLE_TESTING
+ qmlRegisterUncreatableType<Pebble>("PebbleTest", 1, 0, "Pebble", "Dont");
+ QQuickView *view = new QQuickView();
+ view->engine()->rootContext()->setContextProperty("pebble", pebble);
+ view->setSource(QUrl("qrc:///testui/PebbleController.qml"));
+ view->show();
+#endif
+
+ connect(pebble, &Pebble::pebbleConnected, this, &PebbleManager::pebbleConnected);
+}
+
+Pebble* PebbleManager::get(const QBluetoothAddress &address)
+{
+ for (int i = 0; i < m_pebbles.count(); i++) {
+ if (m_pebbles.at(i)->address() == address) {
+ return m_pebbles.at(i);
+ }
+ }
+ return nullptr;
+}
diff --git a/rockworkd/pebblemanager.h b/rockworkd/pebblemanager.h
new file mode 100644
index 0000000..9387ff7
--- /dev/null
+++ b/rockworkd/pebblemanager.h
@@ -0,0 +1,35 @@
+#ifndef PEBBLEMANAGER_H
+#define PEBBLEMANAGER_H
+
+#include "libpebble/pebble.h"
+#include "libpebble/bluez/bluezclient.h"
+
+#include <QObject>
+
+class PebbleManager : public QObject
+{
+ Q_OBJECT
+public:
+ explicit PebbleManager(QObject *parent = 0);
+
+ QList<Pebble*> pebbles() const;
+ Pebble* get(const QBluetoothAddress &address);
+
+signals:
+ void pebbleAdded(Pebble *pebble);
+ void pebbleRemoved(Pebble *pebble);
+
+private slots:
+ void loadPebbles();
+
+ void pebbleConnected();
+
+private:
+ void setupPebble(Pebble *pebble);
+
+ BluezClient *m_bluezClient;
+
+ QList<Pebble*> m_pebbles;
+};
+
+#endif // PEBBLEMANAGER_H
diff --git a/rockworkd/platformintegration/testing/testingplatform.cpp b/rockworkd/platformintegration/testing/testingplatform.cpp
new file mode 100644
index 0000000..aa0c45a
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testingplatform.cpp
@@ -0,0 +1,63 @@
+#include "testingplatform.h"
+
+#include <QQuickView>
+#include <QDebug>
+#include <QQmlContext>
+
+TestingPlatform::TestingPlatform(QObject *parent):
+ PlatformInterface(parent)
+{
+ m_view = new QQuickView();
+ m_view->rootContext()->setContextProperty("handler", this);
+ qmlRegisterUncreatableType<Pebble>("PebbleTest", 1, 0, "Pebble", "Dont");
+ m_view->setSource(QUrl("qrc:///testui/Main.qml"));
+ m_view->show();
+}
+
+void TestingPlatform::sendMusicControlCommand(MusicControlButton command)
+{
+ qDebug() << "Testing platform received music command from pebble" << command;
+}
+
+MusicMetaData TestingPlatform::musicMetaData() const
+{
+ return MusicMetaData("TestArtist", "TestAlbum", "TestTitle");
+}
+
+void TestingPlatform::sendNotification(int type, const QString &from, const QString &subject, const QString &text)
+{
+ qDebug() << "Injecting mock notification" << type;
+ Notification n("test_app_" + QString::number(type));
+ n.setSourceName("Test button " + QString::number(type));
+ n.setSender(from);
+ n.setSubject(subject);
+ n.setBody(text);
+ n.setActToken("tralala");
+ emit notificationReceived(n);
+}
+
+void TestingPlatform::fakeIncomingCall(uint cookie, const QString &number, const QString &name)
+{
+ emit incomingCall(cookie, number, name);
+}
+
+void TestingPlatform::endCall(uint cookie, bool missed)
+{
+ emit callEnded(cookie, missed);
+}
+
+void TestingPlatform::hangupCall(uint cookie)
+{
+ qDebug() << "Testing platform received a hangup call event";
+ emit callEnded(cookie, false);
+}
+
+QList<CalendarEvent> TestingPlatform::organizerItems() const
+{
+ return QList<CalendarEvent>();
+}
+
+void TestingPlatform::actionTriggered(const QString &actToken)
+{
+ qDebug() << "action triggered" << actToken;
+}
diff --git a/rockworkd/platformintegration/testing/testingplatform.h b/rockworkd/platformintegration/testing/testingplatform.h
new file mode 100644
index 0000000..8c820a0
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testingplatform.h
@@ -0,0 +1,31 @@
+#ifndef TESTINGPLATFORM_H
+#define TESTINGPLATFORM_H
+
+#include "libpebble/platforminterface.h"
+
+class QQuickView;
+
+class TestingPlatform : public PlatformInterface
+{
+ Q_OBJECT
+public:
+ explicit TestingPlatform(QObject *parent = 0);
+
+ void sendMusicControlCommand(MusicControlButton command) override;
+ MusicMetaData musicMetaData() const override;
+
+ Q_INVOKABLE void sendNotification(int type, const QString &from, const QString &subject, const QString &text);
+ Q_INVOKABLE void fakeIncomingCall(uint cookie, const QString &number, const QString &name);
+ Q_INVOKABLE void endCall(uint cookie, bool missed);
+
+ void hangupCall(uint cookie) override;
+
+ QList<CalendarEvent> organizerItems() const override;
+ void actionTriggered(const QString &actToken) override;
+signals:
+
+private:
+ QQuickView *m_view;
+};
+
+#endif // TESTINGPLATFORM_H
diff --git a/rockworkd/platformintegration/testing/testui.qrc b/rockworkd/platformintegration/testing/testui.qrc
new file mode 100644
index 0000000..bc0a45f
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testui.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/">
+ <file>testui/Main.qml</file>
+ <file>testui/PebbleController.qml</file>
+ </qresource>
+</RCC>
diff --git a/rockworkd/platformintegration/testing/testui/Main.qml b/rockworkd/platformintegration/testing/testui/Main.qml
new file mode 100644
index 0000000..e520ca4
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testui/Main.qml
@@ -0,0 +1,87 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.3
+import PebbleTest 1.0
+
+Row {
+ Column {
+ spacing: 10
+ Button {
+ text: "Generic Notification"
+ onClicked: {
+ handler.sendNotification(0, "Bro Coly", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Email Notification"
+ onClicked: {
+ handler.sendNotification(1, "Tom Ato", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "SMS with no subject"
+ onClicked: {
+ handler.sendNotification(2, "Tom Ato", "", "TestText")
+ }
+ }
+
+ Button {
+ text: "Facebook Notification"
+ onClicked: {
+ handler.sendNotification(3, "Cole Raby", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Twitter Notification"
+ onClicked: {
+ handler.sendNotification(4, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Telegram Notification"
+ onClicked: {
+ handler.sendNotification(5, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "WhatsApp Notification"
+ onClicked: {
+ handler.sendNotification(6, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+ Button {
+ text: "Hangout Notification"
+ onClicked: {
+ handler.sendNotification(7, "Horse Reddish", "TestSubject", "TestText")
+ }
+ }
+
+ }
+
+ Column {
+ spacing: 10
+ Button {
+ text: "Fake incoming phone call"
+ onClicked: {
+ handler.fakeIncomingCall(1, "123456789", "TestCaller")
+ }
+ }
+ Button {
+ text: "pick up incoming phone call"
+ onClicked: {
+ handler.callStarted(1)
+ }
+ }
+ Button {
+ text: "hang up incoming phone call"
+ onClicked: {
+ handler.endCall(1, false)
+ }
+ }
+ Button {
+ text: "miss incoming phone call"
+ onClicked: {
+ handler.endCall(1, true)
+ }
+ }
+ }
+}
diff --git a/rockworkd/platformintegration/testing/testui/PebbleController.qml b/rockworkd/platformintegration/testing/testui/PebbleController.qml
new file mode 100644
index 0000000..78861d8
--- /dev/null
+++ b/rockworkd/platformintegration/testing/testui/PebbleController.qml
@@ -0,0 +1,44 @@
+import QtQuick 2.4
+import QtQuick.Controls 1.3
+import PebbleTest 1.0
+
+Column {
+ spacing: 10
+ Label {
+ text: pebble.name
+ width: parent.width
+ }
+
+ Button {
+ text: "Insert Timeline Pin"
+ onClicked: {
+ pebble.insertTimelinePin();
+ }
+ }
+ Button {
+ text: "Create Reminder"
+ onClicked: {
+ pebble.insertReminder();
+ }
+ }
+ Button {
+ text: "Clear Timeline"
+ onClicked: {
+ pebble.clearTimeline();
+ }
+ }
+ Button {
+ text: "take screenshot"
+ onClicked: {
+ pebble.requestScreenshot();
+ }
+ }
+
+ Button {
+ text: "dump logs"
+ onClicked: {
+ pebble.dumpLogs();
+ }
+ }
+}
+
diff --git a/rockworkd/platformintegration/ubuntu/callchannelobserver.cpp b/rockworkd/platformintegration/ubuntu/callchannelobserver.cpp
new file mode 100644
index 0000000..e3d852c
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/callchannelobserver.cpp
@@ -0,0 +1,165 @@
+#include "callchannelobserver.h"
+
+#include <TelepathyQt/Contact>
+#include <TelepathyQt/PendingContactInfo>
+
+#include <QContactFetchRequest>
+#include <QContactPhoneNumber>
+#include <QContactFilter>
+#include <QContactDetail>
+#include <QContactDisplayLabel>
+
+QTCONTACTS_USE_NAMESPACE
+
+TelepathyMonitor::TelepathyMonitor(QObject *parent):
+ QObject(parent)
+{
+ Tp::registerTypes();
+ QTimer::singleShot(0, this, &TelepathyMonitor::accountManagerSetup);
+ m_contactManager = new QContactManager("galera");
+ m_contactManager->setParent(this);
+}
+
+void TelepathyMonitor::hangupCall(uint cookie)
+{
+ if (m_currentCalls.contains(cookie)) {
+ m_currentCalls.value(cookie)->hangup();
+ }
+}
+
+void TelepathyMonitor::accountManagerSetup()
+{
+ m_accountManager = Tp::AccountManager::create(Tp::AccountFactory::create(QDBusConnection::sessionBus(),
+ Tp::Account::FeatureCore),
+ Tp::ConnectionFactory::create(QDBusConnection::sessionBus(),
+ Tp::Connection::FeatureCore));
+ connect(m_accountManager->becomeReady(),
+ SIGNAL(finished(Tp::PendingOperation*)),
+ SLOT(accountManagerReady(Tp::PendingOperation*)));
+}
+
+void TelepathyMonitor::accountManagerReady(Tp::PendingOperation* operation)
+{
+ if (operation->isError()) {
+ qDebug() << "TelepathyMonitor: accountManager init error.";
+ QTimer::singleShot(1000, this, &TelepathyMonitor::accountManagerSetup); // again
+ return;
+ }
+ qDebug() << "Telepathy account manager ready";
+
+ foreach (const Tp::AccountPtr& account, m_accountManager->allAccounts()) {
+ connect(account->becomeReady(Tp::Account::FeatureCapabilities),
+ SIGNAL(finished(Tp::PendingOperation*)),
+ SLOT(accountReady(Tp::PendingOperation*)));
+ }
+
+ connect(m_accountManager.data(), SIGNAL(newAccount(Tp::AccountPtr)), SLOT(newAccount(Tp::AccountPtr)));
+}
+
+void TelepathyMonitor::newAccount(const Tp::AccountPtr& account)
+{
+ connect(account->becomeReady(Tp::Account::FeatureCapabilities),
+ SIGNAL(finished(Tp::PendingOperation*)),
+ SLOT(accountReady(Tp::PendingOperation*)));
+}
+
+void TelepathyMonitor::accountReady(Tp::PendingOperation* operation)
+{
+ if (operation->isError()) {
+ qDebug() << "TelepathyAccount: Operation failed (accountReady)";
+ return;
+ }
+
+ Tp::PendingReady* pendingReady = qobject_cast<Tp::PendingReady*>(operation);
+ if (pendingReady == 0) {
+ qDebug() << "Rejecting account because could not understand ready status";
+ return;
+ }
+ checkAndAddAccount(Tp::AccountPtr::qObjectCast(pendingReady->proxy()));
+}
+
+void TelepathyMonitor::onCallStarted(Tp::CallChannelPtr callChannel)
+{
+ // Haven't figured how to send outgoing calls to pebble yet... discard it
+ if (callChannel->initiatorContact()->id().isEmpty()) {
+ qWarning() << "ignoring phone call. looks like it's an outgoing one";
+ return;
+ }
+
+ m_cookie++;
+ m_currentCalls.insert(m_cookie, callChannel.data());
+ m_currentCallStates.insert(m_cookie, Tp::CallStateInitialising);
+
+ callChannel->becomeReady(Tp::CallChannel::FeatureCallState);
+
+ connect(callChannel.data(), &Tp::CallChannel::callStateChanged, this, &TelepathyMonitor::callStateChanged);
+
+ QString number = callChannel->initiatorContact()->id();
+ qDebug() << "call started" << number;
+
+ // try to match the contact info
+ QContactFetchRequest *request = new QContactFetchRequest(this);
+ request->setFilter(QContactPhoneNumber::match(number));
+
+ // lambda function to update the notification
+ QObject::connect(request, &QContactAbstractRequest::stateChanged, [this, request, number](QContactAbstractRequest::State state) {
+ qDebug() << "request returned";
+ if (!request || state != QContactAbstractRequest::FinishedState) {
+ qDebug() << "error fetching contact" << state;
+ return;
+ }
+
+ QContact contact;
+
+ // create the snap decision only after the contact match finishes
+ if (request->contacts().size() > 0) {
+ // use the first match
+ contact = request->contacts().at(0);
+
+ qDebug() << "have contact" << contact.detail<QContactDisplayLabel>().label();
+ emit this->incomingCall(m_cookie, number, contact.detail<QContactDisplayLabel>().label());
+ } else {
+ qDebug() << "unknown contact" << number;
+ emit this->incomingCall(m_cookie, number, QString());
+ }
+ });
+
+ request->setManager(m_contactManager);
+ request->start();
+}
+
+void TelepathyMonitor::callStateChanged(Tp::CallState state)
+{
+ qDebug() << "call state changed1";
+ Tp::CallChannel *channel = qobject_cast<Tp::CallChannel*>(sender());
+ uint cookie = m_currentCalls.key(channel);
+
+ qDebug() << "call state changed2" << state << "cookie:" << cookie;
+
+ switch (state) {
+ case Tp::CallStateActive:
+ emit callStarted(cookie);
+ m_currentCallStates[cookie] = Tp::CallStateActive;
+ break;
+ case Tp::CallStateEnded: {
+ Tp::CallState oldState = m_currentCallStates.value(cookie);
+ emit callEnded(cookie, oldState != Tp::CallStateActive);
+ m_currentCalls.take(cookie);
+ m_currentCallStates.take(cookie);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void TelepathyMonitor::checkAndAddAccount(const Tp::AccountPtr& account)
+{
+ Tp::ConnectionCapabilities caps = account->capabilities();
+ // TODO: Later on we will need to filter for the right capabilities, and also allow dynamic account detection
+ // Don't check caps for now as a workaround for https://bugs.launchpad.net/ubuntu/+source/media-hub/+bug/1409125
+ // at least until we are able to find out the root cause of it (check rev 107 for the caps check)
+ auto tcm = new TelepathyCallMonitor(account);
+ connect(tcm, &TelepathyCallMonitor::callStarted, this, &TelepathyMonitor::onCallStarted);
+ m_callMonitors.append(tcm);
+}
diff --git a/rockworkd/platformintegration/ubuntu/callchannelobserver.h b/rockworkd/platformintegration/ubuntu/callchannelobserver.h
new file mode 100644
index 0000000..cc2b7aa
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/callchannelobserver.h
@@ -0,0 +1,74 @@
+#ifndef CALLCHANNELOBSERVER_H
+#define CALLCHANNELOBSERVER_H
+
+#include <TelepathyQt/AccountManager>
+#include <TelepathyQt/SimpleCallObserver>
+#include <TelepathyQt/PendingOperation>
+#include <TelepathyQt/PendingReady>
+#include <TelepathyQt/PendingAccount>
+#include <TelepathyQt/CallChannel>
+
+#include <QContactManager>
+
+QTCONTACTS_USE_NAMESPACE
+
+class TelepathyCallMonitor : public QObject
+{
+ Q_OBJECT
+public:
+ TelepathyCallMonitor(const Tp::AccountPtr& account):
+ mAccount(account),
+ mCallObserver(Tp::SimpleCallObserver::create(mAccount)) {
+ connect(mCallObserver.data(), SIGNAL(callStarted(Tp::CallChannelPtr)), SIGNAL(callStarted(Tp::CallChannelPtr)));
+// connect(mCallObserver.data(), SIGNAL(callEnded(Tp::CallChannelPtr,QString,QString)), SIGNAL(callEnded()));
+// connect(mCallObserver.data(), SIGNAL(streamedMediaCallStarted(Tp::StreamedMediaChannelPtr)), SIGNAL(offHook()));
+// connect(mCallObserver.data(), SIGNAL(streamedMediaCallEnded(Tp::StreamedMediaChannelPtr,QString,QString)), SIGNAL(onHook()));
+ }
+
+signals:
+ void callStarted(Tp::CallChannelPtr callChannel);
+// void callEnded();
+
+private:
+ Tp::AccountPtr mAccount;
+ Tp::SimpleCallObserverPtr mCallObserver;
+};
+
+class TelepathyMonitor: public QObject
+{
+ Q_OBJECT
+public:
+ TelepathyMonitor(QObject *parent = 0);
+
+ void hangupCall(uint cookie);
+
+private slots:
+ void accountManagerSetup();
+ void accountManagerReady(Tp::PendingOperation* operation);
+
+ void newAccount(const Tp::AccountPtr& account);
+ void accountReady(Tp::PendingOperation* operation);
+
+ void onCallStarted(Tp::CallChannelPtr callChannel);
+ void callStateChanged(Tp::CallState state);
+
+signals:
+ void incomingCall(uint cookie, const QString &number, const QString &name);
+ void callStarted(uint cookie);
+ void callEnded(uint cookie, bool missed);
+
+private:
+ void checkAndAddAccount(const Tp::AccountPtr& account);
+
+private:
+ Tp::AccountManagerPtr m_accountManager;
+ QList<TelepathyCallMonitor*> m_callMonitors;
+ QContactManager *m_contactManager;
+
+ QHash<uint, Tp::CallChannel*> m_currentCalls;
+ QHash<uint, Tp::CallState> m_currentCallStates;
+
+ uint m_cookie = 0;
+};
+
+#endif // CALLCHANNELOBSERVER_H
diff --git a/rockworkd/platformintegration/ubuntu/organizeradapter.cpp b/rockworkd/platformintegration/ubuntu/organizeradapter.cpp
new file mode 100644
index 0000000..853403a
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/organizeradapter.cpp
@@ -0,0 +1,74 @@
+#include "organizeradapter.h"
+
+#include <QOrganizerItemFetchRequest>
+#include <QDebug>
+#include <QOrganizerEventOccurrence>
+#include <QOrganizerItemDetail>
+
+QTORGANIZER_USE_NAMESPACE
+
+#define MANAGER "eds"
+#define MANAGER_FALLBACK "memory"
+
+OrganizerAdapter::OrganizerAdapter(QObject *parent) : QObject(parent)
+{
+ QString envManager(qgetenv("ALARM_BACKEND"));
+ if (envManager.isEmpty())
+ envManager = MANAGER;
+ if (!QOrganizerManager::availableManagers().contains(envManager)) {
+ envManager = MANAGER_FALLBACK;
+ }
+ m_manager = new QOrganizerManager(envManager);
+ m_manager->setParent(this);
+ connect(m_manager, &QOrganizerManager::dataChanged, this, &OrganizerAdapter::refresh);
+}
+
+void OrganizerAdapter::refresh()
+{
+ QList<CalendarEvent> items;
+ foreach (const QOrganizerItem &item, m_manager->items()) {
+ QOrganizerEvent organizerEvent(item);
+ if (organizerEvent.displayLabel().isEmpty()) {
+ continue;
+ }
+ CalendarEvent event;
+ event.setId(organizerEvent.id().toString());
+ event.setTitle(organizerEvent.displayLabel());
+ event.setDescription(organizerEvent.description());
+ event.setStartTime(organizerEvent.startDateTime());
+ event.setEndTime(organizerEvent.endDateTime());
+ event.setLocation(organizerEvent.location());
+ event.setComment(organizerEvent.comments().join(";"));
+ QStringList attendees;
+ foreach (const QOrganizerItemDetail &attendeeDetail, organizerEvent.details(QOrganizerItemDetail::TypeEventAttendee)) {
+ attendees.append(attendeeDetail.value(QOrganizerItemDetail::TypeEventAttendee + 1).toString());
+ }
+ event.setGuests(attendees);
+ event.setRecurring(organizerEvent.recurrenceRules().count() > 0);
+
+ items.append(event);
+
+ quint64 startTimestamp = QDateTime::currentMSecsSinceEpoch();
+ startTimestamp -= (1000 * 60 * 60 * 24 * 7);
+
+ foreach (const QOrganizerItem &occurranceItem, m_manager->itemOccurrences(item, QDateTime::fromMSecsSinceEpoch(startTimestamp), QDateTime::currentDateTime().addDays(7))) {
+ QOrganizerEventOccurrence organizerOccurrance(occurranceItem);
+ event.generateNewUuid();
+ event.setId(organizerOccurrance.id().toString());
+ event.setStartTime(organizerOccurrance.startDateTime());
+ event.setEndTime(organizerOccurrance.endDateTime());
+ items.append(event);
+ }
+ }
+
+ if (m_items != items) {
+ m_items = items;
+ emit itemsChanged(m_items);
+ }
+
+}
+
+QList<CalendarEvent> OrganizerAdapter::items() const
+{
+ return m_items;
+}
diff --git a/rockworkd/platformintegration/ubuntu/organizeradapter.h b/rockworkd/platformintegration/ubuntu/organizeradapter.h
new file mode 100644
index 0000000..2ce8e4d
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/organizeradapter.h
@@ -0,0 +1,33 @@
+#ifndef ORGANIZERADAPTER_H
+#define ORGANIZERADAPTER_H
+
+#include "libpebble/calendarevent.h"
+
+#include <QObject>
+
+#include <QOrganizerManager>
+#include <QOrganizerAbstractRequest>
+#include <QOrganizerEvent>
+
+QTORGANIZER_USE_NAMESPACE
+
+class OrganizerAdapter : public QObject
+{
+ Q_OBJECT
+public:
+ explicit OrganizerAdapter(QObject *parent = 0);
+
+ QList<CalendarEvent> items() const;
+
+public slots:
+ void refresh();
+
+signals:
+ void itemsChanged(const QList<CalendarEvent> &items);
+
+private:
+ QOrganizerManager *m_manager;
+ QList<CalendarEvent> m_items;
+};
+
+#endif // ORGANIZERADAPTER_H
diff --git a/rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp b/rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp
new file mode 100644
index 0000000..b43509e
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/syncmonitorclient.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * This file is part of sync-monitor.
+ *
+ * sync-monitor is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * contact-service-app is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QDebug>
+#include <QTimer>
+
+#include "syncmonitorclient.h"
+
+#define SYNCMONITOR_DBUS_SERVICE_NAME "com.canonical.SyncMonitor"
+#define SYNCMONITOR_DBUS_OBJECT_PATH "/com/canonical/SyncMonitor"
+#define SYNCMONITOR_DBUS_INTERFACE "com.canonical.SyncMonitor"
+
+
+SyncMonitorClient::SyncMonitorClient(QObject *parent)
+ : QObject(parent),
+ m_iface(0)
+{
+ m_iface = new QDBusInterface(SYNCMONITOR_DBUS_SERVICE_NAME,
+ SYNCMONITOR_DBUS_OBJECT_PATH,
+ SYNCMONITOR_DBUS_INTERFACE);
+ if (m_iface->lastError().isValid()) {
+ qWarning() << "Fail to connect with sync monitor:" << m_iface->lastError();
+ return;
+ }
+
+ connect(m_iface, SIGNAL(stateChanged()), SIGNAL(stateChanged()));
+ connect(m_iface, SIGNAL(enabledServicesChanged()), SIGNAL(enabledServicesChanged()));
+ m_iface->call("attach");
+}
+
+SyncMonitorClient::~SyncMonitorClient()
+{
+ if (m_iface) {
+ m_iface->call("detach");
+ delete m_iface;
+ m_iface = 0;
+ }
+}
+
+QString SyncMonitorClient::state() const
+{
+ if (m_iface) {
+ return m_iface->property("state").toString();
+ } else {
+ return "";
+ }
+}
+
+QStringList SyncMonitorClient::enabledServices() const
+{
+ if (m_iface) {
+ return m_iface->property("enabledServices").toStringList();
+ } else {
+ return QStringList();
+ }
+}
+
+/*!
+ Start a new sync for specified services
+*/
+void SyncMonitorClient::sync(const QStringList &services)
+{
+ if (m_iface) {
+ qDebug() << "starting sync!";
+ m_iface->call("sync", services);
+ }
+}
+
+/*!
+ Cancel current sync for specified services
+*/
+void SyncMonitorClient::cancel(const QStringList &services)
+{
+ if (m_iface) {
+ m_iface->call("cancel", services);
+ }
+}
+
+/*!
+ Chek if a specific service is enabled or not
+*/
+bool SyncMonitorClient::serviceIsEnabled(const QString &service)
+{
+ return enabledServices().contains(service);
+}
diff --git a/rockworkd/platformintegration/ubuntu/syncmonitorclient.h b/rockworkd/platformintegration/ubuntu/syncmonitorclient.h
new file mode 100644
index 0000000..1587ba5
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/syncmonitorclient.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * This file is part of sync-monitor.
+ *
+ * sync-monitor is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * contact-service-app is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SYNCMONITOR_QML_H
+#define SYNCMONITOR_QML_H
+
+#include <QObject>
+#include <QDBusInterface>
+
+class SyncMonitorClient : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString state READ state NOTIFY stateChanged)
+ Q_PROPERTY(QStringList enabledServices READ enabledServices NOTIFY enabledServicesChanged)
+
+public:
+ SyncMonitorClient(QObject *parent = 0);
+ ~SyncMonitorClient();
+
+ QString state() const;
+ QStringList enabledServices() const;
+
+Q_SIGNALS:
+ void stateChanged();
+ void enabledServicesChanged();
+
+public Q_SLOTS:
+ void sync(const QStringList &services);
+ void cancel(const QStringList &services);
+ bool serviceIsEnabled(const QString &service);
+
+private:
+ QDBusInterface *m_iface;
+};
+
+#endif
diff --git a/rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp b/rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp
new file mode 100644
index 0000000..7c060b1
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/ubuntuplatform.cpp
@@ -0,0 +1,232 @@
+#include "ubuntuplatform.h"
+
+#include "callchannelobserver.h"
+#include "organizeradapter.h"
+#include "syncmonitorclient.h"
+
+#include <QDBusConnection>
+#include <QDBusConnectionInterface>
+#include <QDebug>
+
+// qmenumodel
+#include "dbus-enums.h"
+#include "liburl-dispatcher-1/url-dispatcher.h"
+
+UbuntuPlatform::UbuntuPlatform(QObject *parent):
+ PlatformInterface(parent),
+ m_volumeActionGroup()
+{
+ // Notifications
+ QDBusConnection::sessionBus().registerObject("/org/freedesktop/Notifications", this, QDBusConnection::ExportAllSlots);
+ m_iface = new QDBusInterface("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus");
+ m_iface->call("AddMatch", "interface='org.freedesktop.Notifications',member='Notify',type='method_call',eavesdrop='true'");
+ m_iface->call("AddMatch", "interface='org.freedesktop.Notifications',member='CloseNotification',type='method_call',eavesdrop='true'");
+
+ // Music
+ setupMusicService();
+ m_volumeActionGroup.setBusType(DBusEnums::SessionBus);
+ m_volumeActionGroup.setBusName("com.canonical.indicator.sound");
+ m_volumeActionGroup.setObjectPath("/com/canonical/indicator/sound");
+ m_volumeActionGroup.QDBusObject::connect();
+ connect(&m_volumeActionGroup, &QDBusActionGroup::statusChanged, [this] {
+ if (m_volumeActionGroup.status() == DBusEnums::Connected) {
+ m_volumeAction = m_volumeActionGroup.action("volume");
+ }
+ });
+
+ // Calls
+ m_telepathyMonitor = new TelepathyMonitor(this);
+ connect(m_telepathyMonitor, &TelepathyMonitor::incomingCall, this, &UbuntuPlatform::incomingCall);
+ connect(m_telepathyMonitor, &TelepathyMonitor::callStarted, this, &UbuntuPlatform::callStarted);
+ connect(m_telepathyMonitor, &TelepathyMonitor::callEnded, this, &UbuntuPlatform::callEnded);
+
+ // Organizer
+ m_organizerAdapter = new OrganizerAdapter(this);
+ m_organizerAdapter->refresh();
+ connect(m_organizerAdapter, &OrganizerAdapter::itemsChanged, this, &UbuntuPlatform::organizerItemsChanged);
+ m_syncMonitorClient = new SyncMonitorClient(this);
+ connect(m_syncMonitorClient, &SyncMonitorClient::stateChanged, [this]() { if (m_syncMonitorClient->state() == "idle") m_organizerAdapter->refresh();});
+ m_syncTimer.start(1000 * 60 * 60);
+ connect(&m_syncTimer, &QTimer::timeout, [this]() { m_syncMonitorClient->sync({"calendar"});});
+ m_syncMonitorClient->sync({"calendar"});
+}
+
+QDBusInterface *UbuntuPlatform::interface() const
+{
+ return m_iface;
+}
+
+uint UbuntuPlatform::Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantHash &hints, int expire_timeout)
+{
+ Q_UNUSED(replaces_id)
+ // Lets directly suppress volume change notifications, network password entries and phone call snap decisions here
+ QStringList hiddenNotifications = {"indicator-sound", "indicator-network"};
+ if (!hiddenNotifications.contains(app_name)) {
+ if (hints.contains("x-canonical-secondary-icon") && hints.value("x-canonical-secondary-icon").toString() == "incoming-call") {
+ qDebug() << "Have a phone call notification. Ignoring it..." << app_name << app_icon;
+ } else {
+ qDebug() << "Notification received" << app_name << app_icon << actions << hints << expire_timeout;
+ Notification n(app_name);
+ if (app_name.contains("twitter")) {
+ n.setType(Notification::NotificationTypeTwitter);
+ n.setSourceName("Twitter");
+ } else if (app_name.contains("dekko")) {
+ n.setType(Notification::NotificationTypeEmail);
+ n.setSourceName("EMail");
+ } else if (app_name.toLower().contains("gmail")) {
+ n.setType(Notification::NotificationTypeGMail);
+ n.setSourceName("GMail");
+ } else if (app_name.contains("facebook")) {
+ n.setType(Notification::NotificationTypeFacebook);
+ n.setSourceName("Facebook");
+ } else if (app_name.contains("telegram")) {
+ n.setType(Notification::NotificationTypeTelegram);
+ n.setSourceName("Telegram");
+ } else if (app_name.toLower().contains("hangout")) {
+ n.setType(Notification::NotificationTypeHangout);
+ n.setSourceName("Hangout");
+ } else if (app_name.contains("indicator-datetime")) {
+ n.setType(Notification::NotificationTypeReminder);
+ n.setSourceName("reminders");
+ } else {
+ n.setType(Notification::NotificationTypeGeneric);
+ }
+ n.setSender(summary);
+ n.setBody(body);
+ foreach (const QString &action, actions) {
+ if (action.contains(QRegExp("^[a-z]*://"))) {
+ n.setActToken(action);
+ break;
+ }
+ }
+ qDebug() << "have act token" << n.actToken();
+
+ emit notificationReceived(n);
+ }
+ }
+ // We never return something. We're just snooping in...
+ setDelayedReply(true);
+ return 0;
+}
+
+void UbuntuPlatform::setupMusicService()
+{
+ if (!m_mprisService.isEmpty()) {
+ disconnect(this, SLOT(mediaPropertiesChanged(QString,QVariantMap,QStringList)));
+ }
+
+ QDBusConnectionInterface *iface = QDBusConnection::sessionBus().interface();
+ const QStringList &services = iface->registeredServiceNames();
+ foreach (QString service, services) {
+ if (service.startsWith("org.mpris.MediaPlayer2.")) {
+ qDebug() << "have mpris service" << service;
+ m_mprisService = service;
+ fetchMusicMetadata();
+ QDBusConnection::sessionBus().connect(m_mprisService, "/org/mpris/MediaPlayer2", "", "PropertiesChanged", this, SLOT(mediaPropertiesChanged(QString,QVariantMap,QStringList)));
+ break;
+ }
+ }
+}
+
+void UbuntuPlatform::sendMusicControlCommand(MusicControlButton controlButton)
+{
+ if (m_mprisService.isEmpty()) {
+ setupMusicService();
+ }
+
+ QString method;
+ switch (controlButton) {
+ case MusicControlPlayPause:
+ method = "PlayPause";
+ break;
+ case MusicControlSkipBack:
+ method = "Previous";
+ break;
+ case MusicControlSkipNext:
+ method = "Next";
+ break;
+ default:
+ ;
+ }
+
+ if (!method.isEmpty()) {
+ QDBusMessage call = QDBusMessage::createMethodCall(m_mprisService, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", method);
+ QDBusError err = QDBusConnection::sessionBus().call(call);
+
+ if (err.isValid()) {
+ qWarning() << "Error calling mpris method on" << m_mprisService << ":" << err.message();
+ }
+ return;
+ }
+
+ int volumeDiff = 0;
+ switch (controlButton) {
+ case MusicControlVolumeUp:
+ volumeDiff = 1;
+ break;
+ case MusicControlVolumeDown:
+ volumeDiff = -1;
+ break;
+ default:
+ ;
+ }
+
+ if (m_volumeAction && volumeDiff != 0) {
+ m_volumeAction->activate(volumeDiff);
+ return;
+ }
+}
+
+MusicMetaData UbuntuPlatform::musicMetaData() const
+{
+ return m_musicMetaData;
+}
+
+void UbuntuPlatform::hangupCall(uint cookie)
+{
+ m_telepathyMonitor->hangupCall(cookie);
+}
+
+QList<CalendarEvent> UbuntuPlatform::organizerItems() const
+{
+ return m_organizerAdapter->items();
+}
+
+void UbuntuPlatform::actionTriggered(const QString &actToken)
+{
+ url_dispatch_send(actToken.toStdString().c_str(), [] (const gchar *, gboolean, gpointer) {}, nullptr);
+}
+
+void UbuntuPlatform::fetchMusicMetadata()
+{
+ if (!m_mprisService.isEmpty()) {
+ QDBusMessage call = QDBusMessage::createMethodCall(m_mprisService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
+ call << "org.mpris.MediaPlayer2.Player" << "Metadata";
+ QDBusPendingCall pcall = QDBusConnection::sessionBus().asyncCall(call);
+ QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pcall, this);
+ connect(watcher, &QDBusPendingCallWatcher::finished, this, &UbuntuPlatform::fetchMusicMetadataFinished);
+ }
+}
+
+void UbuntuPlatform::fetchMusicMetadataFinished(QDBusPendingCallWatcher *watcher)
+{
+ watcher->deleteLater();
+ QDBusReply<QDBusVariant> reply = watcher->reply();
+ if (reply.isValid()) {
+ QVariantMap curMetadata = qdbus_cast<QVariantMap>(reply.value().variant().value<QDBusArgument>());
+ m_musicMetaData.artist = curMetadata.value("xesam:artist").toString();
+ m_musicMetaData.album = curMetadata.value("xesam:album").toString();
+ m_musicMetaData.title = curMetadata.value("xesam:title").toString();
+ emit musicMetadataChanged(m_musicMetaData);
+ } else {
+ qWarning() << reply.error().message();
+ }
+}
+
+void UbuntuPlatform::mediaPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps)
+{
+ Q_UNUSED(interface)
+ Q_UNUSED(changedProps)
+ Q_UNUSED(invalidatedProps)
+ fetchMusicMetadata();
+}
diff --git a/rockworkd/platformintegration/ubuntu/ubuntuplatform.h b/rockworkd/platformintegration/ubuntu/ubuntuplatform.h
new file mode 100644
index 0000000..5679a42
--- /dev/null
+++ b/rockworkd/platformintegration/ubuntu/ubuntuplatform.h
@@ -0,0 +1,62 @@
+#ifndef UBUNTUPLATFORM_H
+#define UBUNTUPLATFORM_H
+
+#include "libpebble/platforminterface.h"
+#include "libpebble/enums.h"
+
+#include <QDBusInterface>
+#include <TelepathyQt/AbstractClientObserver>
+
+#include <qdbusactiongroup.h>
+#include <qstateaction.h>
+
+class QDBusPendingCallWatcher;
+class TelepathyMonitor;
+class OrganizerAdapter;
+class SyncMonitorClient;
+
+class UbuntuPlatform : public PlatformInterface, public QDBusContext
+{
+ Q_OBJECT
+ Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Notifications")
+ Q_PROPERTY(QDBusInterface* interface READ interface)
+
+
+public:
+ UbuntuPlatform(QObject *parent = 0);
+ QDBusInterface* interface() const;
+
+ void sendMusicControlCommand(MusicControlButton controlButton) override;
+ MusicMetaData musicMetaData() const override;
+
+ void hangupCall(uint cookie) override;
+
+ QList<CalendarEvent> organizerItems() const override;
+
+ void actionTriggered(const QString &actToken) override;
+
+public slots:
+ uint Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantHash &hints, int expire_timeout);
+
+
+private slots:
+ void setupMusicService();
+ void fetchMusicMetadata();
+ void fetchMusicMetadataFinished(QDBusPendingCallWatcher *watcher);
+ void mediaPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps);
+
+private:
+ QDBusInterface *m_iface;
+
+ QString m_mprisService;
+ MusicMetaData m_musicMetaData;
+ QDBusActionGroup m_volumeActionGroup;
+ QStateAction *m_volumeAction = nullptr;
+
+ TelepathyMonitor *m_telepathyMonitor;
+ OrganizerAdapter *m_organizerAdapter;
+ SyncMonitorClient *m_syncMonitorClient;
+ QTimer m_syncTimer;
+};
+
+#endif // UBUNTUPLATFORM_H
diff --git a/rockworkd/rockworkd.pro b/rockworkd/rockworkd.pro
new file mode 100644
index 0000000..e56ced9
--- /dev/null
+++ b/rockworkd/rockworkd.pro
@@ -0,0 +1,146 @@
+QT += core bluetooth dbus network contacts qml location organizer
+QT -= gui
+
+include(../version.pri)
+load(ubuntu-click)
+
+TARGET = rockworkd
+CONFIG += c++11
+#CONFIG -= app_bundle
+
+TEMPLATE = app
+
+#TODO: figure why pkgconfig doesn't work in the click chroot
+#CONFIG += link_pkgconfig
+#PKGCONFIG += url-dispatcher-1
+INCLUDEPATH += /usr/lib/arm-linux-gnueabihf/glib-2.0/include /usr/lib/x86_64-linux-gnu/glib-2.0/include/ /usr/include/glib-2.0/
+LIBS += -lurl-dispatcher
+
+INCLUDEPATH += /usr/include/telepathy-qt5/ /usr/include/qmenumodel/
+LIBS += -lquazip-qt5 -ltelepathy-qt5 -lqmenumodel
+
+SOURCES += main.cpp \
+ libpebble/watchconnection.cpp \
+ libpebble/pebble.cpp \
+ libpebble/watchdatareader.cpp \
+ libpebble/watchdatawriter.cpp \
+ libpebble/notificationendpoint.cpp \
+ libpebble/musicendpoint.cpp \
+ libpebble/phonecallendpoint.cpp \
+ libpebble/musicmetadata.cpp \
+ libpebble/jskit/jskitmanager.cpp \
+ libpebble/jskit/jskitconsole.cpp \
+ libpebble/jskit/jskitgeolocation.cpp \
+ libpebble/jskit/jskitlocalstorage.cpp \
+ libpebble/jskit/jskitpebble.cpp \
+ libpebble/jskit/jskitxmlhttprequest.cpp \
+ libpebble/jskit/jskittimer.cpp \
+ libpebble/jskit/jskitperformance.cpp \
+ libpebble/appinfo.cpp \
+ libpebble/appmanager.cpp \
+ libpebble/appmsgmanager.cpp \
+ libpebble/uploadmanager.cpp \
+ libpebble/bluez/bluezclient.cpp \
+ libpebble/bluez/bluez_agentmanager1.cpp \
+ libpebble/bluez/bluez_adapter1.cpp \
+ libpebble/bluez/bluez_device1.cpp \
+ libpebble/bluez/freedesktop_objectmanager.cpp \
+ libpebble/bluez/freedesktop_properties.cpp \
+ core.cpp \
+ pebblemanager.cpp \
+ dbusinterface.cpp \
+# Platform integration part
+ platformintegration/ubuntu/ubuntuplatform.cpp \
+ platformintegration/ubuntu/callchannelobserver.cpp \
+ libpebble/blobdb.cpp \
+ libpebble/timelineitem.cpp \
+ libpebble/notification.cpp \
+ platformintegration/ubuntu/organizeradapter.cpp \
+ libpebble/calendarevent.cpp \
+ platformintegration/ubuntu/syncmonitorclient.cpp \
+ libpebble/appmetadata.cpp \
+ libpebble/appdownloader.cpp \
+ libpebble/screenshotendpoint.cpp \
+ libpebble/firmwaredownloader.cpp \
+ libpebble/bundle.cpp \
+ libpebble/watchlogendpoint.cpp \
+ libpebble/ziphelper.cpp \
+ libpebble/healthparams.cpp \
+ libpebble/dataloggingendpoint.cpp
+
+HEADERS += \
+ libpebble/watchconnection.h \
+ libpebble/pebble.h \
+ libpebble/watchdatareader.h \
+ libpebble/watchdatawriter.h \
+ libpebble/notificationendpoint.h \
+ libpebble/musicendpoint.h \
+ libpebble/musicmetadata.h \
+ libpebble/phonecallendpoint.h \
+ libpebble/platforminterface.h \
+ libpebble/jskit/jskitmanager.h \
+ libpebble/jskit/jskitconsole.h \
+ libpebble/jskit/jskitgeolocation.h \
+ libpebble/jskit/jskitlocalstorage.h \
+ libpebble/jskit/jskitpebble.h \
+ libpebble/jskit/jskitxmlhttprequest.h \
+ libpebble/jskit/jskittimer.h \
+ libpebble/jskit/jskitperformance.h \
+ libpebble/appinfo.h \
+ libpebble/appmanager.h \
+ libpebble/appmsgmanager.h \
+ libpebble/uploadmanager.h \
+ libpebble/bluez/bluezclient.h \
+ libpebble/bluez/bluez_agentmanager1.h \
+ libpebble/bluez/bluez_adapter1.h \
+ libpebble/bluez/bluez_device1.h \
+ libpebble/bluez/freedesktop_objectmanager.h \
+ libpebble/bluez/freedesktop_properties.h \
+ core.h \
+ pebblemanager.h \
+ dbusinterface.h \
+# Platform integration part
+ platformintegration/ubuntu/ubuntuplatform.h \
+ platformintegration/ubuntu/callchannelobserver.h \
+ libpebble/blobdb.h \
+ libpebble/timelineitem.h \
+ libpebble/notification.h \
+ platformintegration/ubuntu/organizeradapter.h \
+ libpebble/calendarevent.h \
+ platformintegration/ubuntu/syncmonitorclient.h \
+ libpebble/appmetadata.h \
+ libpebble/appdownloader.h \
+ libpebble/enums.h \
+ libpebble/screenshotendpoint.h \
+ libpebble/firmwaredownloader.h \
+ libpebble/bundle.h \
+ libpebble/watchlogendpoint.h \
+ libpebble/ziphelper.h \
+ libpebble/healthparams.h \
+ libpebble/dataloggingendpoint.h
+
+testing: {
+ SOURCES += platformintegration/testing/testingplatform.cpp
+ HEADERS += platformintegration/testing/testingplatform.h
+ RESOURCES += platformintegration/testing/testui.qrc
+ DEFINES += ENABLE_TESTING
+ QT += qml quick
+}
+
+libs.files = /usr/lib/arm-linux-gnueabihf/libQt5Bluetooth.so.5.4.1 \
+ /usr/lib/arm-linux-gnueabihf/libQt5Bluetooth.so.5 \
+ /usr/lib/arm-linux-gnueabihf/libquazip-qt5.so.1.0.0 \
+ /usr/lib/arm-linux-gnueabihf/libquazip-qt5.so.1
+libs.path = $${UBUNTU_CLICK_BINARY_PATH}/..
+INSTALLS += libs
+
+
+# Default rules for deployment.
+target.path = $${UBUNTU_CLICK_BINARY_PATH}
+INSTALLS+=target
+
+QMAKE_POST_LINK = sed -i s/@VERSION@/$$VERSION/g $$OUT_PWD/../manifest.json || exit 0
+#QMAKE_POST_LINK = echo $$OUT_PWD/../manifest.json > /tmp/huhu;
+
+RESOURCES += \
+ libpebble/jskit/jsfiles.qrc
diff --git a/version.pri b/version.pri
new file mode 100644
index 0000000..4925049
--- /dev/null
+++ b/version.pri
@@ -0,0 +1,2 @@
+VERSION = "0.92"
+DEFINES += VERSION=\\\"$$VERSION\\\"