diff options
| author | Andrew Branson <andrew.branson@cern.ch> | 2016-02-11 23:55:16 +0100 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@cern.ch> | 2016-02-11 23:55:16 +0100 |
| commit | 29aaea2d80a9eb1715b6cddfac2d2aacf76358bd (patch) | |
| tree | 012795b6bec16c72f38d33cff46324c9a0225868 | |
launchpad ~mzanetti/rockwork/trunk r87
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 Binary files differnew file mode 100644 index 0000000..d1207c9 --- /dev/null +++ b/rockwork/artwork/bianca-black.png diff --git a/rockwork/artwork/bianca-silver.png b/rockwork/artwork/bianca-silver.png Binary files differnew file mode 100644 index 0000000..2d003e8 --- /dev/null +++ b/rockwork/artwork/bianca-silver.png diff --git a/rockwork/artwork/black-20mm-hole.png b/rockwork/artwork/black-20mm-hole.png Binary files differnew file mode 100644 index 0000000..ff61e66 --- /dev/null +++ b/rockwork/artwork/black-20mm-hole.png diff --git a/rockwork/artwork/bobby-black.png b/rockwork/artwork/bobby-black.png Binary files differnew file mode 100644 index 0000000..83177b5 --- /dev/null +++ b/rockwork/artwork/bobby-black.png diff --git a/rockwork/artwork/bobby-gold.png b/rockwork/artwork/bobby-gold.png Binary files differnew file mode 100644 index 0000000..d97f2f4 --- /dev/null +++ b/rockwork/artwork/bobby-gold.png diff --git a/rockwork/artwork/bobby-silver.png b/rockwork/artwork/bobby-silver.png Binary files differnew file mode 100644 index 0000000..44efdf8 --- /dev/null +++ b/rockwork/artwork/bobby-silver.png 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 Binary files differnew file mode 100644 index 0000000..acf3439 --- /dev/null +++ b/rockwork/artwork/snowy-black.png diff --git a/rockwork/artwork/snowy-red.png b/rockwork/artwork/snowy-red.png Binary files differnew file mode 100644 index 0000000..b0bdc8e --- /dev/null +++ b/rockwork/artwork/snowy-red.png diff --git a/rockwork/artwork/snowy-white.png b/rockwork/artwork/snowy-white.png Binary files differnew file mode 100644 index 0000000..3bfe6d1 --- /dev/null +++ b/rockwork/artwork/snowy-white.png diff --git a/rockwork/artwork/spalding-14mm-black.png b/rockwork/artwork/spalding-14mm-black.png Binary files differnew file mode 100644 index 0000000..47b5b03 --- /dev/null +++ b/rockwork/artwork/spalding-14mm-black.png diff --git a/rockwork/artwork/spalding-14mm-rose-gold.png b/rockwork/artwork/spalding-14mm-rose-gold.png Binary files differnew file mode 100644 index 0000000..8775cf1 --- /dev/null +++ b/rockwork/artwork/spalding-14mm-rose-gold.png diff --git a/rockwork/artwork/spalding-14mm-silver.png b/rockwork/artwork/spalding-14mm-silver.png Binary files differnew file mode 100644 index 0000000..bcc5f16 --- /dev/null +++ b/rockwork/artwork/spalding-14mm-silver.png diff --git a/rockwork/artwork/spalding-20mm-black.png b/rockwork/artwork/spalding-20mm-black.png Binary files differnew file mode 100644 index 0000000..d00a1f7 --- /dev/null +++ b/rockwork/artwork/spalding-20mm-black.png diff --git a/rockwork/artwork/spalding-20mm-silver.png b/rockwork/artwork/spalding-20mm-silver.png Binary files differnew file mode 100644 index 0000000..18b0e02 --- /dev/null +++ b/rockwork/artwork/spalding-20mm-silver.png diff --git a/rockwork/artwork/tintin-black.png b/rockwork/artwork/tintin-black.png Binary files differnew file mode 100644 index 0000000..dcf2c31 --- /dev/null +++ b/rockwork/artwork/tintin-black.png diff --git a/rockwork/artwork/tintin-blue.png b/rockwork/artwork/tintin-blue.png Binary files differnew file mode 100644 index 0000000..eca2d3b --- /dev/null +++ b/rockwork/artwork/tintin-blue.png diff --git a/rockwork/artwork/tintin-green.png b/rockwork/artwork/tintin-green.png Binary files differnew file mode 100644 index 0000000..17df060 --- /dev/null +++ b/rockwork/artwork/tintin-green.png diff --git a/rockwork/artwork/tintin-grey.png b/rockwork/artwork/tintin-grey.png Binary files differnew file mode 100644 index 0000000..4f9988b --- /dev/null +++ b/rockwork/artwork/tintin-grey.png diff --git a/rockwork/artwork/tintin-orange.png b/rockwork/artwork/tintin-orange.png Binary files differnew file mode 100644 index 0000000..5956126 --- /dev/null +++ b/rockwork/artwork/tintin-orange.png diff --git a/rockwork/artwork/tintin-pink.png b/rockwork/artwork/tintin-pink.png Binary files differnew file mode 100644 index 0000000..ee69d67 --- /dev/null +++ b/rockwork/artwork/tintin-pink.png diff --git a/rockwork/artwork/tintin-red.png b/rockwork/artwork/tintin-red.png Binary files differnew file mode 100644 index 0000000..6c7b7e2 --- /dev/null +++ b/rockwork/artwork/tintin-red.png diff --git a/rockwork/artwork/tintin-white.png b/rockwork/artwork/tintin-white.png Binary files differnew file mode 100644 index 0000000..912ea19 --- /dev/null +++ b/rockwork/artwork/tintin-white.png 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 Binary files differnew file mode 100644 index 0000000..1a354b4 --- /dev/null +++ b/rockwork/snowywhite.png 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 ¬ification) +{ + TimelineAttribute::IconID iconId = TimelineAttribute::IconIDDefaultBell; + TimelineAttribute::Color color = TimelineAttribute::ColorRed; + QString muteName; + switch (notification.type()) { + case Notification::NotificationTypeAlarm: + iconId = TimelineAttribute::IconIDAlarm; + muteName = "Alarms"; + break; + case Notification::NotificationTypeFacebook: + iconId = TimelineAttribute::IconIDFacebook; + color = TimelineAttribute::ColorBlue; + muteName = "facebook"; + break; + case Notification::NotificationTypeGMail: + iconId = TimelineAttribute::IconIDGMail; + muteName = "GMail"; + break; + case Notification::NotificationTypeHangout: + iconId = TimelineAttribute::IconIDHangout; + color = TimelineAttribute::ColorGreen; + muteName = "Hangout"; + break; + case Notification::NotificationTypeMissedCall: + iconId = TimelineAttribute::IconIDDefaultMissedCall; + muteName = "call notifications"; + break; + case Notification::NotificationTypeMusic: + iconId = TimelineAttribute::IconIDMusic; + muteName = "music"; + break; + case Notification::NotificationTypeReminder: + iconId = TimelineAttribute::IconIDReminder; + muteName = "reminders"; + break; + case Notification::NotificationTypeTelegram: + iconId = TimelineAttribute::IconIDTelegram; + color = TimelineAttribute::ColorLightBlue; + muteName = "Telegram"; + break; + case Notification::NotificationTypeTwitter: + iconId = TimelineAttribute::IconIDTwitter; + color = TimelineAttribute::ColorBlue2; + muteName = "Twitter"; + break; + case Notification::NotificationTypeWeather: + iconId = TimelineAttribute::IconIDWeather; + muteName = "Weather"; + break; + case Notification::NotificationTypeWhatsApp: + iconId = TimelineAttribute::IconIDWhatsApp; + color = TimelineAttribute::ColorGreen; + muteName = "WhatsApp"; + break; + case Notification::NotificationTypeSMS: + muteName = "SMS"; + iconId = TimelineAttribute::IconIDDefaultBell; + break; + case Notification::NotificationTypeEmail: + default: + muteName = "e mails"; + iconId = TimelineAttribute::IconIDDefaultBell; + break; + } + + QUuid itemUuid = QUuid::createUuid(); + TimelineItem timelineItem(itemUuid, TimelineItem::TypeNotification); + timelineItem.setFlags(TimelineItem::FlagSingleEvent); + + TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, notification.sender().left(64).toUtf8()); + timelineItem.appendAttribute(titleAttribute); + + TimelineAttribute subjectAttribute(TimelineAttribute::TypeSubtitle, notification.subject().left(64).toUtf8()); + timelineItem.appendAttribute(subjectAttribute); + + TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, notification.body().toUtf8()); + timelineItem.appendAttribute(bodyAttribute); + + TimelineAttribute iconAttribute(TimelineAttribute::TypeTinyIcon, iconId); + timelineItem.appendAttribute(iconAttribute); + + TimelineAttribute colorAttribute(TimelineAttribute::TypeColor, color); + timelineItem.appendAttribute(colorAttribute); + + TimelineAction dismissAction(0, TimelineAction::TypeDismiss); + TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss"); + dismissAction.appendAttribute(dismissAttribute); + timelineItem.appendAction(dismissAction); + + TimelineAction muteAction(1, TimelineAction::TypeGeneric); + TimelineAttribute muteActionAttribute(TimelineAttribute::TypeTitle, "Mute " + muteName.toUtf8()); + muteAction.appendAttribute(muteActionAttribute); + timelineItem.appendAction(muteAction); + + if (!notification.actToken().isEmpty()) { + TimelineAction actAction(2, TimelineAction::TypeGeneric); + TimelineAttribute actActionAttribute(TimelineAttribute::TypeTitle, "Open on phone"); + actAction.appendAttribute(actActionAttribute); + timelineItem.appendAction(actAction); + } + + insert(BlobDB::BlobDBIdNotification, timelineItem); + m_notificationSources.insert(itemUuid, notification); +} + +void BlobDB::insertTimelinePin(const QUuid &uuid, TimelineItem::Layout layout, const QDateTime &startTime, const QDateTime &endTime, const QString &title, const QString &desctiption, const QMap<QString, QString> fields, bool recurring) +{ +// TimelineItem item(TimelineItem::TypePin, TimelineItem::FlagSingleEvent, QDateTime::currentDateTime().addMSecs(1000 * 60 * 2), 60); + + qDebug() << "inserting timeline pin:" << title << startTime << endTime; + int duration = (endTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch()) / 1000 / 60; + TimelineItem item(uuid, TimelineItem::TypePin, TimelineItem::FlagSingleEvent, startTime, duration); + item.setLayout(layout); + + TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, title.toUtf8()); + item.appendAttribute(titleAttribute); + + if (!desctiption.isEmpty()) { + TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, desctiption.left(128).toUtf8()); + item.appendAttribute(bodyAttribute); + } + +// TimelineAttribute iconAttribute(TimelineAttribute::TypeTinyIcon, TimelineAttribute::IconIDTelegram); +// item.appendAttribute(iconAttribute); + + if (!fields.isEmpty()) { + TimelineAttribute fieldNames(TimelineAttribute::TypeFieldNames, fields.keys()); + item.appendAttribute(fieldNames); + + TimelineAttribute fieldValues(TimelineAttribute::TypeFieldValues, fields.values()); + item.appendAttribute(fieldValues); + } + + if (recurring) { + TimelineAttribute guess(TimelineAttribute::TypeRecurring, 0x01); + item.appendAttribute(guess); + } + + TimelineAction dismissAction(0, TimelineAction::TypeDismiss); + TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss"); + dismissAction.appendAttribute(dismissAttribute); + item.appendAction(dismissAction); + + insert(BlobDB::BlobDBIdPin, item); +} + +void BlobDB::removeTimelinePin(const QUuid &uuid) +{ + qDebug() << "Removing timeline pin:" << uuid; + remove(BlobDBId::BlobDBIdPin, uuid); +} + +void BlobDB::insertReminder() +{ + + TimelineItem item(TimelineItem::TypeReminder, TimelineItem::FlagSingleEvent, QDateTime::currentDateTime().addMSecs(1000 * 60 * 2), 0); + + TimelineAttribute titleAttribute(TimelineAttribute::TypeTitle, "ReminderTitle"); + item.appendAttribute(titleAttribute); + + TimelineAttribute subjectAttribute(TimelineAttribute::TypeSubtitle, "ReminderSubtitle"); + item.appendAttribute(subjectAttribute); + + TimelineAttribute bodyAttribute(TimelineAttribute::TypeBody, "ReminderBody"); + item.appendAttribute(bodyAttribute); + + QByteArray data; + data.append(0x07); data.append('\0'); data.append('\0'); data.append(0x80); + TimelineAttribute guessAttribute(TimelineAttribute::TypeTinyIcon, data); + item.appendAttribute(guessAttribute); + qDebug() << "attrib" << guessAttribute.serialize(); + + TimelineAction dismissAction(0, TimelineAction::TypeDismiss); + TimelineAttribute dismissAttribute(TimelineAttribute::TypeTitle, "Dismiss"); + dismissAction.appendAttribute(dismissAttribute); + item.appendAction(dismissAction); + + insert(BlobDB::BlobDBIdReminder, item); + // qDebug() << "adding timeline item" << ddd.toHex(); + +} + +void BlobDB::clearTimeline() +{ + foreach (CalendarEvent entry, m_calendarEntries) { + entry.removeFromCache(m_blobDBStoragePath); + } + m_calendarEntries.clear(); + clear(BlobDB::BlobDBIdPin); +} + +void BlobDB::syncCalendar(const QList<CalendarEvent> &events) +{ + qDebug() << "BlobDB: Starting calendar sync for" << events.count() << "entries"; + QList<CalendarEvent> itemsToSync; + QList<CalendarEvent> itemsToAdd; + QList<CalendarEvent> itemsToDelete; + + // Filter out invalid items + foreach (const CalendarEvent &event, events) { + if (event.startTime().isValid() && event.endTime().isValid() + && event.startTime().addDays(2) > QDateTime::currentDateTime() + && QDateTime::currentDateTime().addDays(5) > event.startTime()) { + itemsToSync.append(event); + } + } + + // Compare events to local ones + foreach (const CalendarEvent &event, itemsToSync) { + CalendarEvent syncedEvent = findCalendarEvent(event.id()); + if (!syncedEvent.isValid()) { + itemsToAdd.append(event); + } else if (!(syncedEvent == event)) { + qDebug() << "event has changed!"; + itemsToDelete.append(syncedEvent); + itemsToAdd.append(event); + } + } + + // Find stale local ones + foreach (const CalendarEvent &event, m_calendarEntries) { + bool found = false; + foreach (const CalendarEvent &tmp, events) { + if (tmp.id() == event.id()) { + found = true; + break; + } + } + if (!found) { + qDebug() << "removing stale timeline entry"; + itemsToDelete.append(event); + } + } + + foreach (const CalendarEvent &event, itemsToDelete) { + removeTimelinePin(event.uuid()); + m_calendarEntries.removeAll(event); + event.removeFromCache(m_blobDBStoragePath); + } + + qDebug() << "adding" << itemsToAdd.count() << "timeline entries"; + foreach (const CalendarEvent &event, itemsToAdd) { + QMap<QString, QString> fields; + if (!event.location().isEmpty()) fields.insert("Location", event.location()); + if (!event.calendar().isEmpty()) fields.insert("Calendar", event.calendar()); + if (!event.comment().isEmpty()) fields.insert("Comments", event.comment()); + if (!event.guests().isEmpty()) fields.insert("Guests", event.guests().join(", ")); + insertTimelinePin(event.uuid(), TimelineItem::LayoutCalendar, event.startTime(), event.endTime(), event.title(), event.description(), fields, event.recurring()); + m_calendarEntries.append(event); + event.saveToCache(m_blobDBStoragePath); + } +} + +void BlobDB::clearApps() +{ + clear(BlobDBId::BlobDBIdApp); + QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat); + s.remove(""); +} + +void BlobDB::insertAppMetaData(const AppInfo &info) +{ + if (!m_pebble->connected()) { + qWarning() << "Pebble is not connected. Cannot install app"; + return; + } + + QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat); + if (s.value(info.uuid().toString(), false).toBool()) { + qWarning() << "App already in DB. Not syncing again"; + return; + } + + AppMetadata metaData = appInfoToMetadata(info, m_pebble->hardwarePlatform()); + + BlobCommand *cmd = new BlobCommand(); + cmd->m_command = BlobDB::OperationInsert; + cmd->m_token = generateToken(); + cmd->m_database = BlobDBIdApp; + + cmd->m_key = metaData.uuid().toRfc4122(); + cmd->m_value = metaData.serialize(); + + m_commandQueue.append(cmd); + sendNext(); +} + +void BlobDB::removeApp(const AppInfo &info) +{ + remove(BlobDBId::BlobDBIdApp, info.uuid()); + QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat); + s.remove(info.uuid().toString()); +} + +void BlobDB::insert(BlobDBId database, const TimelineItem &item) +{ + if (!m_connection->isConnected()) { + return; + } + BlobCommand *cmd = new BlobCommand(); + cmd->m_command = BlobDB::OperationInsert; + cmd->m_token = generateToken(); + cmd->m_database = database; + + cmd->m_key = item.itemId().toRfc4122(); + cmd->m_value = item.serialize(); + + m_commandQueue.append(cmd); + sendNext(); +} + +void BlobDB::remove(BlobDB::BlobDBId database, const QUuid &uuid) +{ + if (!m_connection->isConnected()) { + return; + } + BlobCommand *cmd = new BlobCommand(); + cmd->m_command = BlobDB::OperationDelete; + cmd->m_token = generateToken(); + cmd->m_database = database; + + cmd->m_key = uuid.toRfc4122(); + + m_commandQueue.append(cmd); + sendNext(); +} + +void BlobDB::clear(BlobDB::BlobDBId database) +{ + BlobCommand *cmd = new BlobCommand(); + cmd->m_command = BlobDB::OperationClear; + cmd->m_token = generateToken(); + cmd->m_database = database; + + m_commandQueue.append(cmd); + sendNext(); +} + +void BlobDB::setHealthParams(const HealthParams &healthParams) +{ + BlobCommand *cmd = new BlobCommand(); + cmd->m_command = BlobDB::OperationInsert; + cmd->m_token = generateToken(); + cmd->m_database = BlobDBIdAppSettings; + + cmd->m_key = "activityPreferences"; + cmd->m_value = healthParams.serialize(); + + qDebug() << "Setting health params. Enabled:" << healthParams.enabled() << cmd->serialize().toHex(); + m_commandQueue.append(cmd); + sendNext(); +} + +void BlobDB::setUnits(bool imperial) +{ + BlobCommand *cmd = new BlobCommand(); + cmd->m_command = BlobDB::OperationInsert; + cmd->m_token = generateToken(); + cmd->m_database = BlobDBIdAppSettings; + + cmd->m_key = "unitsDistance"; + WatchDataWriter writer(&cmd->m_value); + writer.write<quint8>(imperial ? 0x01 : 0x00); + + m_commandQueue.append(cmd); + sendNext(); +} + +void BlobDB::blobCommandReply(const QByteArray &data) +{ + WatchDataReader reader(data); + quint16 token = reader.readLE<quint16>(); + quint8 status = reader.read<quint8>(); + if (m_currentCommand->m_token != token) { + qWarning() << "Received reply for unexpected token"; + } else if (status != 0x01) { + qWarning() << "Blob Command failed:" << status; + } else { // All is well + if (m_currentCommand->m_database == BlobDBIdApp && m_currentCommand->m_command == OperationInsert) { + QSettings s(m_blobDBStoragePath + "/appsyncstate.conf", QSettings::IniFormat); + QUuid appUuid = QUuid::fromRfc4122(m_currentCommand->m_key); + s.setValue(appUuid.toString(), true); + emit appInserted(appUuid); + } + } + + if (m_currentCommand && token == m_currentCommand->m_token) { + delete m_currentCommand; + m_currentCommand = nullptr; + sendNext(); + } +} + +void BlobDB::actionInvoked(const QByteArray &actionReply) +{ + WatchDataReader reader(actionReply); + TimelineAction::Type actionType = (TimelineAction::Type)reader.read<quint8>(); + QUuid notificationId = reader.readUuid(); + quint8 actionId = reader.read<quint8>(); + quint8 param = reader.read<quint8>(); // Is this correct? So far I've only seen 0x00 in here + + // Not sure what to do with those yet + Q_UNUSED(actionType) + Q_UNUSED(param) + + qDebug() << "Action invoked" << actionId << actionReply.toHex(); + + Status status = StatusError; + QList<TimelineAttribute> attributes; + + Notification notification = m_notificationSources.value(notificationId); + QString sourceId = notification.sourceId(); + if (sourceId.isEmpty()) { + status = StatusError; + } else { + switch (actionId) { + case 1: { // Mute source + TimelineAttribute textAttribute(TimelineAttribute::TypeSubtitle, "Muted!"); + attributes.append(textAttribute); +// TimelineAttribute iconAttribute(TimelineAttribute::TypeLargeIcon, TimelineAttribute::IconIDTelegram); +// attributes.append(iconAttribute); + emit muteSource(sourceId); + status = StatusSuccess; + break; + } + case 2: { // Open on phone + TimelineAttribute textAttribute(TimelineAttribute::TypeSubtitle, "Opened!"); + attributes.append(textAttribute); + qDebug() << "opening" << notification.actToken(); + emit actionTriggered(notification.actToken()); + status = StatusSuccess; + } + } + } + + QByteArray reply; + reply.append(0x11); // Length of id & status code + reply.append(notificationId.toRfc4122()); + reply.append(status); + reply.append(attributes.count()); + foreach (const TimelineAttribute &attrib, attributes) { + reply.append(attrib.serialize()); + } + m_connection->writeToPebble(WatchConnection::EndpointActionHandler, reply); +} + +void BlobDB::sendActionReply() +{ + +} + +void BlobDB::sendNext() +{ + if (m_currentCommand || m_commandQueue.isEmpty()) { + return; + } + m_currentCommand = m_commandQueue.takeFirst(); + m_connection->writeToPebble(WatchConnection::EndpointBlobDB, m_currentCommand->serialize()); +} + +quint16 BlobDB::generateToken() +{ + return (qrand() % ((int)pow(2, 16) - 2)) + 1; +} + +AppMetadata BlobDB::appInfoToMetadata(const AppInfo &info, HardwarePlatform hardwarePlatform) +{ + QString binaryFile = info.file(AppInfo::FileTypeApplication, hardwarePlatform); + QFile f(binaryFile); + if (!f.open(QFile::ReadOnly)) { + qWarning() << "Error opening app binary"; + return AppMetadata(); + } + QByteArray data = f.read(512); + WatchDataReader reader(data); + qDebug() << "Header:" << reader.readFixedString(8); + qDebug() << "struct Major version:" << reader.read<quint8>(); + qDebug() << "struct Minor version:" << reader.read<quint8>(); + quint8 sdkVersionMajor = reader.read<quint8>(); + qDebug() << "sdk Major version:" << sdkVersionMajor; + quint8 sdkVersionMinor = reader.read<quint8>(); + qDebug() << "sdk Minor version:" << sdkVersionMinor; + quint8 appVersionMajor = reader.read<quint8>(); + qDebug() << "app Major version:" << appVersionMajor; + quint8 appVersionMinor = reader.read<quint8>(); + qDebug() << "app Minor version:" << appVersionMinor; + qDebug() << "size:" << reader.readLE<quint16>(); + qDebug() << "offset:" << reader.readLE<quint32>(); + qDebug() << "crc:" << reader.readLE<quint32>(); + QString appName = reader.readFixedString(32); + qDebug() << "App name:" << appName; + qDebug() << "Vendor name:" << reader.readFixedString(32); + quint32 icon = reader.readLE<quint32>(); + qDebug() << "Icon:" << icon; + qDebug() << "Symbol table address:" << reader.readLE<quint32>(); + quint32 flags = reader.readLE<quint32>(); + qDebug() << "Flags:" << flags; + qDebug() << "Num relocatable entries:" << reader.readLE<quint32>(); + + f.close(); + qDebug() << "app data" << data.toHex(); + + AppMetadata metadata; + metadata.setUuid(info.uuid()); + metadata.setFlags(flags); + metadata.setAppVersion(appVersionMajor, appVersionMinor); + metadata.setSDKVersion(sdkVersionMajor, sdkVersionMinor); + metadata.setAppFaceBgColor(0); + metadata.setAppFaceTemplateId(0); + metadata.setAppName(appName); + metadata.setIcon(icon); + return metadata; + +} + +CalendarEvent BlobDB::findCalendarEvent(const QString &id) +{ + foreach (const CalendarEvent &entry, m_calendarEntries) { + if (entry.id() == id) { + return entry; + } + } + return CalendarEvent(); +} + +QByteArray BlobDB::BlobCommand::serialize() const +{ + QByteArray ret; + ret.append((quint8)m_command); + ret.append(m_token & 0xFF); ret.append(((m_token >> 8) & 0xFF)); + ret.append((quint8)m_database); + + if (m_command == BlobDB::OperationInsert || m_command == BlobDB::OperationDelete) { + ret.append(m_key.length() & 0xFF); + ret.append(m_key); + } + if (m_command == BlobDB::OperationInsert) { + ret.append(m_value.length() & 0xFF); ret.append((m_value.length() >> 8) & 0xFF); // value length + ret.append(m_value); + } + + return ret; +} diff --git a/rockworkd/libpebble/blobdb.h b/rockworkd/libpebble/blobdb.h new file mode 100644 index 0000000..b1db403 --- /dev/null +++ b/rockworkd/libpebble/blobdb.h @@ -0,0 +1,108 @@ +#ifndef BLOBDB_H +#define BLOBDB_H + +#include "watchconnection.h" +#include "pebble.h" +#include "timelineitem.h" +#include "healthparams.h" +#include "appmetadata.h" + +#include <QObject> +#include <QDateTime> +#include <QOrganizerEvent> + +QTORGANIZER_USE_NAMESPACE + + +class BlobDB : public QObject +{ + Q_OBJECT +public: + enum BlobDBId { + BlobDBIdTest = 0, + BlobDBIdPin = 1, + BlobDBIdApp = 2, + BlobDBIdReminder = 3, + BlobDBIdNotification = 4, + BlobDBIdAppSettings = 7 + + }; + enum Operation { + OperationInsert = 0x01, + OperationDelete = 0x04, + OperationClear = 0x05 + }; + + enum Status { + StatusSuccess = 0x00, + StatusError = 0x01 + }; + + + explicit BlobDB(Pebble *pebble, WatchConnection *connection); + + void insertNotification(const Notification ¬ification); + void insertTimelinePin(const QUuid &uuid, TimelineItem::Layout layout, const QDateTime &startTime, const QDateTime &endTime, const QString &title, const QString &desctiption, const QMap<QString, QString> fields, bool recurring); + void removeTimelinePin(const QUuid &uuid); + void insertReminder(); + void clearTimeline(); + void syncCalendar(const QList<CalendarEvent> &events); + + void clearApps(); + void insertAppMetaData(const AppInfo &info); + void removeApp(const AppInfo &info); + + void insert(BlobDBId database, const TimelineItem &item); + void remove(BlobDBId database, const QUuid &uuid); + void clear(BlobDBId database); + + void setHealthParams(const HealthParams &healthParams); + void setUnits(bool imperial); + +private slots: + void blobCommandReply(const QByteArray &data); + void actionInvoked(const QByteArray &data); + void sendActionReply(); + void sendNext(); + +signals: + void muteSource(const QString &sourceId); + void actionTriggered(const QString &actToken); + void appInserted(const QUuid &uuid); + +private: + quint16 generateToken(); + AppMetadata appInfoToMetadata(const AppInfo &info, HardwarePlatform hardwarePlatform); + +private: + + class BlobCommand: public PebblePacket + { + public: + BlobDB::Operation m_command; // quint8 + quint16 m_token; + BlobDB::BlobDBId m_database; + + QByteArray m_key; + QByteArray m_value; + + QByteArray serialize() const override; + }; + + Pebble *m_pebble; + WatchConnection *m_connection; + + QHash<QUuid, Notification> m_notificationSources; + + QList<CalendarEvent> m_calendarEntries; + CalendarEvent findCalendarEvent(const QString &id); + + HealthParams m_healthParams; + + BlobCommand *m_currentCommand = nullptr; + QList<BlobCommand*> m_commandQueue; + + QString m_blobDBStoragePath; +}; + +#endif // BLOBDB_H diff --git a/rockworkd/libpebble/bluez/bluez_adapter1.cpp b/rockworkd/libpebble/bluez/bluez_adapter1.cpp new file mode 100644 index 0000000..a386af1 --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_adapter1.cpp @@ -0,0 +1,26 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c BluezAdapter1 -p bluez_adapter1 -v org.bluez.Adapter1.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +#include "bluez_adapter1.h" + +/* + * Implementation of interface class BluezAdapter1 + */ + +BluezAdapter1::BluezAdapter1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +BluezAdapter1::~BluezAdapter1() +{ +} + diff --git a/rockworkd/libpebble/bluez/bluez_adapter1.h b/rockworkd/libpebble/bluez/bluez_adapter1.h new file mode 100644 index 0000000..8690075 --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_adapter1.h @@ -0,0 +1,66 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c BluezAdapter1 -p bluez_adapter1 -v org.bluez.Adapter1.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef BLUEZ_ADAPTER1_H_1442480417 +#define BLUEZ_ADAPTER1_H_1442480417 + +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> + +/* + * Proxy class for interface org.bluez.Adapter1 + */ +class BluezAdapter1: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.bluez.Adapter1"; } + +public: + BluezAdapter1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0); + + ~BluezAdapter1(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> RemoveDevice(const QDBusObjectPath &device) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(device); + return asyncCallWithArgumentList(QStringLiteral("RemoveDevice"), argumentList); + } + + inline QDBusPendingReply<> StartDiscovery() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("StartDiscovery"), argumentList); + } + + inline QDBusPendingReply<> StopDiscovery() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("StopDiscovery"), argumentList); + } + +Q_SIGNALS: // SIGNALS +}; + +namespace org { + namespace bluez { + typedef ::BluezAdapter1 Adapter1; + } +} +#endif diff --git a/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp b/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp new file mode 100644 index 0000000..630953b --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_agentmanager1.cpp @@ -0,0 +1,26 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c BluezAgentManager1 -p bluez_agentmanager1 org.bluez.AgentManager1.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +#include "bluez_agentmanager1.h" + +/* + * Implementation of interface class BluezAgentManager1 + */ + +BluezAgentManager1::BluezAgentManager1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +BluezAgentManager1::~BluezAgentManager1() +{ +} + diff --git a/rockworkd/libpebble/bluez/bluez_agentmanager1.h b/rockworkd/libpebble/bluez/bluez_agentmanager1.h new file mode 100644 index 0000000..5f50e0d --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_agentmanager1.h @@ -0,0 +1,68 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c BluezAgentManager1 -p bluez_agentmanager1 org.bluez.AgentManager1.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef BLUEZ_AGENTMANAGER1_H_1442489332 +#define BLUEZ_AGENTMANAGER1_H_1442489332 + +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> + +/* + * Proxy class for interface org.bluez.AgentManager1 + */ +class BluezAgentManager1: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.bluez.AgentManager1"; } + +public: + BluezAgentManager1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0); + + ~BluezAgentManager1(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> RegisterAgent(const QDBusObjectPath &agent, const QString &capability) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(agent) << QVariant::fromValue(capability); + return asyncCallWithArgumentList(QStringLiteral("RegisterAgent"), argumentList); + } + + inline QDBusPendingReply<> RequestDefaultAgent(const QDBusObjectPath &agent) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(agent); + return asyncCallWithArgumentList(QStringLiteral("RequestDefaultAgent"), argumentList); + } + + inline QDBusPendingReply<> UnregisterAgent(const QDBusObjectPath &agent) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(agent); + return asyncCallWithArgumentList(QStringLiteral("UnregisterAgent"), argumentList); + } + +Q_SIGNALS: // SIGNALS +}; + +namespace org { + namespace bluez { + typedef ::BluezAgentManager1 AgentManager1; + } +} +#endif diff --git a/rockworkd/libpebble/bluez/bluez_device1.cpp b/rockworkd/libpebble/bluez/bluez_device1.cpp new file mode 100644 index 0000000..b5ee0f8 --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_device1.cpp @@ -0,0 +1,26 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c BluezDevice1 -p bluez_device1 org.bluez.Device1.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +#include "bluez_device1.h" + +/* + * Implementation of interface class BluezDevice1 + */ + +BluezDevice1::BluezDevice1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +BluezDevice1::~BluezDevice1() +{ +} + diff --git a/rockworkd/libpebble/bluez/bluez_device1.h b/rockworkd/libpebble/bluez/bluez_device1.h new file mode 100644 index 0000000..c9eaa1f --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_device1.h @@ -0,0 +1,85 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c BluezDevice1 -p bluez_device1 org.bluez.Device1.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef BLUEZ_DEVICE1_H_1442480478 +#define BLUEZ_DEVICE1_H_1442480478 + +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> + +/* + * Proxy class for interface org.bluez.Device1 + */ +class BluezDevice1: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.bluez.Device1"; } + +public: + BluezDevice1(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0); + + ~BluezDevice1(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> CancelPairing() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("CancelPairing"), argumentList); + } + + inline QDBusPendingReply<> Connect() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("Connect"), argumentList); + } + + inline QDBusPendingReply<> ConnectProfile(const QString &UUID) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(UUID); + return asyncCallWithArgumentList(QStringLiteral("ConnectProfile"), argumentList); + } + + inline QDBusPendingReply<> Disconnect() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("Disconnect"), argumentList); + } + + inline QDBusPendingReply<> DisconnectProfile(const QString &UUID) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(UUID); + return asyncCallWithArgumentList(QStringLiteral("DisconnectProfile"), argumentList); + } + + inline QDBusPendingReply<> Pair() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("Pair"), argumentList); + } + +Q_SIGNALS: // SIGNALS +}; + +namespace org { + namespace bluez { + typedef ::BluezDevice1 Device1; + } +} +#endif diff --git a/rockworkd/libpebble/bluez/bluez_helper.h b/rockworkd/libpebble/bluez/bluez_helper.h new file mode 100644 index 0000000..363f7ae --- /dev/null +++ b/rockworkd/libpebble/bluez/bluez_helper.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * +*/ + +#ifndef BLUEZ_HELPER_H_ +#define BLUEZ_HELPER_H_ + +#include <QObject> +#include <QDBusObjectPath> + +typedef QMap<QString, QVariantMap> InterfaceList; +typedef QMap<QDBusObjectPath, InterfaceList> ManagedObjectList; + +Q_DECLARE_METATYPE(InterfaceList) +Q_DECLARE_METATYPE(ManagedObjectList) + +#endif diff --git a/rockworkd/libpebble/bluez/bluezclient.cpp b/rockworkd/libpebble/bluez/bluezclient.cpp new file mode 100644 index 0000000..8cdf848 --- /dev/null +++ b/rockworkd/libpebble/bluez/bluezclient.cpp @@ -0,0 +1,84 @@ +#include "bluezclient.h" +#include "dbus-shared.h" + +#include <QDBusConnection> +#include <QDBusReply> +#include <QDebug> + +BluezClient::BluezClient(QObject *parent): + QObject(parent), + m_dbus(QDBusConnection::systemBus()), + m_bluezManager("org.bluez", "/", m_dbus), + m_bluezAgentManager("org.bluez", "/org/bluez", m_dbus) +{ + qDBusRegisterMetaType<InterfaceList>(); + qDBusRegisterMetaType<ManagedObjectList>(); + + if (m_bluezManager.isValid()) { + connect(&m_bluezManager, SIGNAL(InterfacesAdded(const QDBusObjectPath&, InterfaceList)), + this, SLOT(slotInterfacesAdded(const QDBusObjectPath&, InterfaceList))); + + connect(&m_bluezManager, SIGNAL(InterfacesRemoved(const QDBusObjectPath&, const QStringList&)), + this, SLOT(slotInterfacesRemoved(const QDBusObjectPath&, const QStringList&))); + + auto objectList = m_bluezManager.GetManagedObjects().argumentAt<0>(); + for (QDBusObjectPath path : objectList.keys()) { + InterfaceList ifaces = objectList.value(path); + if (ifaces.contains(BLUEZ_DEVICE_IFACE)) { + QString candidatePath = path.path(); + + auto properties = ifaces.value(BLUEZ_DEVICE_IFACE); + addDevice(path, properties); + } + } + } +} + +QList<Device> BluezClient::pairedPebbles() const +{ + QList<Device> ret; + if (m_bluezManager.isValid()) { + foreach (const Device &dev, m_devices) { + ret << dev; + } + } + return ret; +} + +void BluezClient::addDevice(const QDBusObjectPath &path, const QVariantMap &properties) +{ + QString address = properties.value("Address").toString(); + QString name = properties.value("Name").toString(); + if (name.startsWith("Pebble") && !name.startsWith("Pebble Time LE") && !name.startsWith("Pebble-LE") && !m_devices.contains(address)) { + qDebug() << "Found new Pebble:" << address << name; + Device device; + device.address = QBluetoothAddress(address); + device.name = name; + device.path = path.path(); + m_devices.insert(path.path(), device); + qDebug() << "emitting added"; + emit devicesChanged(); + } +} + +void BluezClient::slotInterfacesAdded(const QDBusObjectPath &path, InterfaceList ifaces) +{ + qDebug() << "Interface added!"; + if (ifaces.contains(BLUEZ_DEVICE_IFACE)) { + auto properties = ifaces.value(BLUEZ_DEVICE_IFACE); + addDevice(path, properties); + } +} + +void BluezClient::slotInterfacesRemoved(const QDBusObjectPath &path, const QStringList &ifaces) +{ + qDebug() << "interfaces removed" << path.path() << ifaces; + if (!ifaces.contains(BLUEZ_DEVICE_IFACE)) { + return; + } + if (m_devices.contains(path.path())) { + m_devices.take(path.path()); + qDebug() << "removing dev"; + emit devicesChanged(); + } +} diff --git a/rockworkd/libpebble/bluez/bluezclient.h b/rockworkd/libpebble/bluez/bluezclient.h new file mode 100644 index 0000000..f8e5749 --- /dev/null +++ b/rockworkd/libpebble/bluez/bluezclient.h @@ -0,0 +1,51 @@ +#ifndef BLUEZCLIENT_H +#define BLUEZCLIENT_H + +#include <QList> +#include <QBluetoothAddress> +#include <QBluetoothLocalDevice> + +#include "bluez_helper.h" +#include "freedesktop_objectmanager.h" +#include "freedesktop_properties.h" +#include "bluez_adapter1.h" +#include "bluez_agentmanager1.h" + +class Device { +public: + QBluetoothAddress address; + QString name; + QString path; +}; + +class BluezClient: public QObject +{ + Q_OBJECT + +public: + BluezClient(QObject *parent = 0); + + + QList<Device> pairedPebbles() const; + +private slots: + void addDevice(const QDBusObjectPath &path, const QVariantMap &properties); + + void slotInterfacesAdded(const QDBusObjectPath&path, InterfaceList ifaces); + void slotInterfacesRemoved(const QDBusObjectPath&path, const QStringList &ifaces); + +signals: + void devicesChanged(); + +private: + QDBusConnection m_dbus; + DBusObjectManagerInterface m_bluezManager; + BluezAgentManager1 m_bluezAgentManager; + BluezAdapter1 *m_bluezAdapter = nullptr; + FreeDesktopProperties *m_bluezAdapterProperties = nullptr; + + + QHash<QString, Device> m_devices; +}; + +#endif // BLUEZCLIENT_H diff --git a/rockworkd/libpebble/bluez/dbus-shared.h b/rockworkd/libpebble/bluez/dbus-shared.h new file mode 100644 index 0000000..01e9699 --- /dev/null +++ b/rockworkd/libpebble/bluez/dbus-shared.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#ifndef USS_DBUS_SHARED_H +#define USS_DBUS_SHARED_H + +#define DBUS_AGENT_PATH "/com/canonical/SettingsBluetoothAgent" +#define DBUS_ADAPTER_AGENT_PATH "/com/canonical/SettingsBluetoothAgent/adapteragent" +#define DBUS_AGENT_CAPABILITY "KeyboardDisplay" + +#define BLUEZ_SERVICE "org.bluez" + +#define BLUEZ_ADAPTER_IFACE "org.bluez.Adapter1" +#define BLUEZ_DEVICE_IFACE "org.bluez.Device1" + +#define watchCall(call, func) \ + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); \ + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, func) + +#endif // USS_DBUS_SHARED_H diff --git a/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp b/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp new file mode 100644 index 0000000..71ca4ce --- /dev/null +++ b/rockworkd/libpebble/bluez/freedesktop_objectmanager.cpp @@ -0,0 +1,26 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -p freedesktop_objectmanager -i bluez_helper.h -v -c DBusObjectManagerInterface org.freedesktop.DBus.ObjectManager.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +#include "freedesktop_objectmanager.h" + +/* + * Implementation of interface class DBusObjectManagerInterface + */ + +DBusObjectManagerInterface::DBusObjectManagerInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +DBusObjectManagerInterface::~DBusObjectManagerInterface() +{ +} + diff --git a/rockworkd/libpebble/bluez/freedesktop_objectmanager.h b/rockworkd/libpebble/bluez/freedesktop_objectmanager.h new file mode 100644 index 0000000..509c5fc --- /dev/null +++ b/rockworkd/libpebble/bluez/freedesktop_objectmanager.h @@ -0,0 +1,58 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -p freedesktop_objectmanager -i bluez_helper.h -v -c DBusObjectManagerInterface org.freedesktop.DBus.ObjectManager.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef FREEDESKTOP_OBJECTMANAGER_H_1442473386 +#define FREEDESKTOP_OBJECTMANAGER_H_1442473386 + +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> +#include "bluez_helper.h" + +/* + * Proxy class for interface org.freedesktop.DBus.ObjectManager + */ +class DBusObjectManagerInterface: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.freedesktop.DBus.ObjectManager"; } + +public: + DBusObjectManagerInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0); + + ~DBusObjectManagerInterface(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<ManagedObjectList> GetManagedObjects() + { + QList<QVariant> argumentList; + return asyncCallWithArgumentList(QStringLiteral("GetManagedObjects"), argumentList); + } + +Q_SIGNALS: // SIGNALS + void InterfacesAdded(const QDBusObjectPath &object_path, InterfaceList interfaces_and_properties); + void InterfacesRemoved(const QDBusObjectPath &object_path, const QStringList &interfaces); +}; + +namespace org { + namespace freedesktop { + namespace DBus { + typedef ::DBusObjectManagerInterface ObjectManager; + } + } +} +#endif diff --git a/rockworkd/libpebble/bluez/freedesktop_properties.cpp b/rockworkd/libpebble/bluez/freedesktop_properties.cpp new file mode 100644 index 0000000..c74347c --- /dev/null +++ b/rockworkd/libpebble/bluez/freedesktop_properties.cpp @@ -0,0 +1,26 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c FreeDesktopProperties -p freedesktop_properties -v org.freedesktop.DBus.Properties.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +#include "freedesktop_properties.h" + +/* + * Implementation of interface class FreeDesktopProperties + */ + +FreeDesktopProperties::FreeDesktopProperties(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +FreeDesktopProperties::~FreeDesktopProperties() +{ +} + diff --git a/rockworkd/libpebble/bluez/freedesktop_properties.h b/rockworkd/libpebble/bluez/freedesktop_properties.h new file mode 100644 index 0000000..a7a655c --- /dev/null +++ b/rockworkd/libpebble/bluez/freedesktop_properties.h @@ -0,0 +1,71 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -c FreeDesktopProperties -p freedesktop_properties -v org.freedesktop.DBus.Properties.xml + * + * qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef FREEDESKTOP_PROPERTIES_H_1442473392 +#define FREEDESKTOP_PROPERTIES_H_1442473392 + +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtDBus/QtDBus> + +/* + * Proxy class for interface org.freedesktop.DBus.Properties + */ +class FreeDesktopProperties: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.freedesktop.DBus.Properties"; } + +public: + FreeDesktopProperties(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = 0); + + ~FreeDesktopProperties(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<QDBusVariant> Get(const QString &interface, const QString &name) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(interface) << QVariant::fromValue(name); + return asyncCallWithArgumentList(QStringLiteral("Get"), argumentList); + } + + inline QDBusPendingReply<QVariantMap> GetAll(const QString &interface) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(interface); + return asyncCallWithArgumentList(QStringLiteral("GetAll"), argumentList); + } + + inline QDBusPendingReply<> Set(const QString &interface, const QString &name, const QDBusVariant &value) + { + QList<QVariant> argumentList; + argumentList << QVariant::fromValue(interface) << QVariant::fromValue(name) << QVariant::fromValue(value); + return asyncCallWithArgumentList(QStringLiteral("Set"), argumentList); + } + +Q_SIGNALS: // SIGNALS + void PropertiesChanged(const QString &interface, const QVariantMap &changed_properties, const QStringList &invalidated_properties); +}; + +namespace org { + namespace freedesktop { + namespace DBus { + typedef ::FreeDesktopProperties Properties; + } + } +} +#endif diff --git a/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml b/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml new file mode 100644 index 0000000..e535c7e --- /dev/null +++ b/rockworkd/libpebble/bluez/org.bluez.AgentManager1.xml @@ -0,0 +1,16 @@ +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node> + <interface name="org.bluez.AgentManager1"> + <method name="RegisterAgent"> + <arg type="o" name="agent"/> + <arg type="s" name="capability"/> + </method> + <method name="UnregisterAgent"> + <arg type="o" name="agent"/> + </method> + <method name="RequestDefaultAgent"> + <arg type="o" name="agent"/> + </method> + </interface> +</node> diff --git a/rockworkd/libpebble/bundle.cpp b/rockworkd/libpebble/bundle.cpp new file mode 100644 index 0000000..64061c8 --- /dev/null +++ b/rockworkd/libpebble/bundle.cpp @@ -0,0 +1,151 @@ +#include "bundle.h" + +#include <QVariantMap> +#include <QFileInfo> +#include <QDebug> +#include <QJsonParseError> + +Bundle::Bundle(const QString &path): + m_path(path) +{ + +} + +QString Bundle::path() const +{ + return m_path; +} + +QString Bundle::file(Bundle::FileType type, HardwarePlatform hardwarePlatform) const +{ + // Those two will always be in the top level dir. HardwarePlatform is irrelevant. + switch (type) { + case FileTypeAppInfo: + return m_path + "/appInfo.js"; + case FileTypeJsApp: + return m_path + "/pebble-js-app.js"; + default: + ; + } + + // For all the others we have to find the manifest file + QList<QString> possibleDirs; + + switch (hardwarePlatform) { + case HardwarePlatformAplite: + if (QFileInfo::exists(path() + "/aplite/")) { + possibleDirs.append("aplite"); + } + possibleDirs.append(""); + break; + case HardwarePlatformBasalt: + if (QFileInfo::exists(path() + "/basalt/")) { + possibleDirs.append("basalt"); + } + possibleDirs.append(""); + break; + case HardwarePlatformChalk: + if (QFileInfo::exists(path() + "/chalk/")) { + possibleDirs.append("chalk"); + } + break; + default: + possibleDirs.append(""); + ; + } + + QString manifestFilename; + QString subDir; + foreach (const QString &dir, possibleDirs) { + if (QFileInfo::exists(m_path + "/" + dir + "/manifest.json")) { + subDir = "/" + dir; + manifestFilename = m_path + subDir + "/manifest.json"; + break; + } + } + if (manifestFilename.isEmpty()) { + qWarning() << "Error finding manifest.json"; + return QString(); + } + + // We want the manifiest file. just return it without parsing it + if (type == FileTypeManifest) { + return manifestFilename; + } + + QFile manifest(manifestFilename); + if (!manifest.open(QFile::ReadOnly)) { + qWarning() << "Error opening" << manifestFilename; + return QString(); + } + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(manifest.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Error parsing" << manifestFilename; + return QString(); + } + + QVariantMap manifestMap = jsonDoc.toVariant().toMap(); + switch (type) { + case FileTypeApplication: + return m_path + subDir + "/" + manifestMap.value("application").toMap().value("name").toString(); + case FileTypeResources: + if (manifestMap.contains("resources")) { + return m_path + subDir + "/" + manifestMap.value("resources").toMap().value("name").toString(); + } + break; + case FileTypeWorker: + if (manifestMap.contains("worker")) { + return m_path + subDir + "/" + manifestMap.value("worker").toMap().value("name").toString(); + } + break; + case FileTypeFirmware: + if (manifestMap.contains("firmware")) { + return m_path + subDir + "/" + manifestMap.value("firmware").toMap().value("name").toString(); + } + break; + default: + ; + } + return QString(); +} + +quint32 Bundle::crc(Bundle::FileType type, HardwarePlatform hardwarePlatform) const +{ + switch (type) { + case FileTypeAppInfo: + case FileTypeJsApp: + case FileTypeManifest: + qWarning() << "Cannot get crc for file type" << type; + return 0; + default: ; + } + + QFile manifest(file(FileTypeManifest, hardwarePlatform)); + if (!manifest.open(QFile::ReadOnly)) { + qWarning() << "Error opening manifest file"; + return 0; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(manifest.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Error parsing manifest file"; + return 0; + } + + QVariantMap manifestMap = jsonDoc.toVariant().toMap(); + switch (type) { + case FileTypeApplication: + return manifestMap.value("application").toMap().value("crc").toUInt(); + case FileTypeResources: + return manifestMap.value("resources").toMap().value("crc").toUInt(); + case FileTypeWorker: + return manifestMap.value("worker").toMap().value("crc").toUInt(); + case FileTypeFirmware: + return manifestMap.value("firmware").toMap().value("crc").toUInt(); + default: + ; + } + return 0; +} diff --git a/rockworkd/libpebble/bundle.h b/rockworkd/libpebble/bundle.h new file mode 100644 index 0000000..5ff5a16 --- /dev/null +++ b/rockworkd/libpebble/bundle.h @@ -0,0 +1,33 @@ +#ifndef BUNDLE_H +#define BUNDLE_H + +#include <QString> + +#include "enums.h" + +class Bundle +{ +public: + enum FileType { + FileTypeAppInfo, + FileTypeJsApp, + FileTypeManifest, + FileTypeApplication, + FileTypeResources, + FileTypeWorker, + FileTypeFirmware + }; + + Bundle(const QString &path = QString()); + + QString path() const; + + QString file(FileType type, HardwarePlatform hardwarePlatform = HardwarePlatformUnknown) const; + quint32 crc(FileType type, HardwarePlatform hardwarePlatform = HardwarePlatformUnknown) const; + +private: + QString m_path; + +}; + +#endif // BUNDLE_H diff --git a/rockworkd/libpebble/calendarevent.cpp b/rockworkd/libpebble/calendarevent.cpp new file mode 100644 index 0000000..ea99b56 --- /dev/null +++ b/rockworkd/libpebble/calendarevent.cpp @@ -0,0 +1,184 @@ +#include "calendarevent.h" + +#include <QSettings> +#include <QFile> +#include <QTimeZone> + +CalendarEvent::CalendarEvent(): + m_uuid(QUuid::createUuid()) +{ +} + +bool CalendarEvent::isValid() const +{ + return !m_id.isNull(); +} + +QString CalendarEvent::id() const +{ + return m_id; +} + +void CalendarEvent::setId(const QString &id) +{ + m_id = id; +} + +QUuid CalendarEvent::uuid() const +{ + return m_uuid; +} + +void CalendarEvent::generateNewUuid() +{ + m_uuid = QUuid::createUuid(); +} + +QString CalendarEvent::title() const +{ + return m_title; +} + +void CalendarEvent::setTitle(const QString &title) +{ + m_title = title; +} + +QString CalendarEvent::description() const +{ + return m_description; +} + +void CalendarEvent::setDescription(const QString &description) +{ + m_description = description; +} + +QDateTime CalendarEvent::startTime() const +{ + return m_startTime; +} + +void CalendarEvent::setStartTime(const QDateTime &startTime) +{ + m_startTime = startTime; +} + +QDateTime CalendarEvent::endTime() const +{ + return m_endTime; +} + +void CalendarEvent::setEndTime(const QDateTime &endTime) +{ + m_endTime = endTime; +} + +QString CalendarEvent::location() const +{ + return m_location; +} + +void CalendarEvent::setLocation(const QString &location) +{ + m_location = location; +} + +QString CalendarEvent::calendar() const +{ + return m_calendar; +} + +void CalendarEvent::setCalendar(const QString &calendar) +{ + m_calendar = calendar; +} + +QString CalendarEvent::comment() const +{ + return m_comment; +} + +void CalendarEvent::setComment(const QString &comment) +{ + m_comment = comment; +} + +QStringList CalendarEvent::guests() const +{ + return m_guests; +} + +void CalendarEvent::setGuests(const QStringList &guests) +{ + m_guests = guests; +} + +bool CalendarEvent::recurring() const +{ + return m_recurring; +} + +void CalendarEvent::setRecurring(bool recurring) +{ + m_recurring = recurring; +} + +bool CalendarEvent::operator==(const CalendarEvent &other) const +{ + // Storing a QDateTime to QSettings seems to lose time zone information. Lets ignore the time zone when + // comparing or we'll never find ourselves again. + QDateTime thisStartTime = m_startTime; + thisStartTime.setTimeZone(other.startTime().timeZone()); + QDateTime thisEndTime = m_endTime; + thisEndTime.setTimeZone(other.endTime().timeZone()); + return m_id == other.id() + && m_title == other.title() + && m_description == other.description() + && thisStartTime == other.startTime() + && thisEndTime == other.endTime() + && m_location == other.location() + && m_calendar == other.calendar() + && m_comment == other.comment() + && m_guests == other.guests() + && m_recurring == other.recurring(); + +} + +void CalendarEvent::saveToCache(const QString &cachePath) const +{ + QSettings s(cachePath + "/calendarevent-" + m_uuid.toString(), QSettings::IniFormat); + s.setValue("id", m_id); + s.setValue("uuid", m_uuid); + s.setValue("title", m_title); + s.setValue("description", m_description); + s.setValue("startTime", m_startTime); + s.setValue("endTime", m_endTime); + s.setValue("location", m_location); + s.setValue("calendar", m_calendar); + s.setValue("comment", m_comment); + s.setValue("guests", m_guests); + s.setValue("recurring", m_recurring); +} + +void CalendarEvent::loadFromCache(const QString &cachePath, const QString &uuid) +{ + m_uuid = uuid; + QSettings s(cachePath + "/calendarevent-" + m_uuid.toString(), QSettings::IniFormat); + m_id = s.value("id").toString(); + m_title = s.value("title").toString(); + m_description = s.value("description").toString(); + m_startTime = s.value("startTime").toDateTime(); + m_endTime = s.value("endTime").toDateTime(); + m_location = s.value("location").toString(); + m_calendar = s.value("calendar").toString(); + m_comment = s.value("comment").toString(); + m_guests = s.value("guests").toStringList(); + m_recurring = s.value("recurring").toBool(); +} + +void CalendarEvent::removeFromCache(const QString &cachePath) const +{ + QFile::remove(cachePath + "/calendarevent-" + m_uuid.toString()); +} + diff --git a/rockworkd/libpebble/calendarevent.h b/rockworkd/libpebble/calendarevent.h new file mode 100644 index 0000000..5361a48 --- /dev/null +++ b/rockworkd/libpebble/calendarevent.h @@ -0,0 +1,69 @@ +#ifndef CALENDAREVENT_H +#define CALENDAREVENT_H + +#include <QString> +#include <QStringList> +#include <QDateTime> +#include <QUuid> + +class CalendarEvent +{ +public: + CalendarEvent(); + + bool isValid() const; + + QString id() const; + void setId(const QString &id); + + QUuid uuid() const; + void generateNewUuid(); + + QString title() const; + void setTitle(const QString &title); + + QString description() const; + void setDescription(const QString &description); + + QDateTime startTime() const; + void setStartTime(const QDateTime &startTime); + + QDateTime endTime() const; + void setEndTime(const QDateTime &endTime); + + QString location() const; + void setLocation(const QString &location); + + QString calendar() const; + void setCalendar(const QString &calendar); + + QString comment() const; + void setComment(const QString &comment); + + QStringList guests() const; + void setGuests(const QStringList &guests); + + bool recurring() const; + void setRecurring(bool recurring); + + bool operator==(const CalendarEvent &other) const; + + void saveToCache(const QString &cachePath) const; + void loadFromCache(const QString &cachePath, const QString &uuid); + void removeFromCache(const QString &cachePath) const; + +private: + QString m_id; + QUuid m_uuid; + QString m_title; + QString m_description; + QDateTime m_startTime; + QDateTime m_endTime; + QString m_location; + QString m_calendar; + QString m_comment; + QStringList m_guests; + bool m_recurring = false; +}; + +#endif // CALENDAREVENT_H diff --git a/rockworkd/libpebble/dataloggingendpoint.cpp b/rockworkd/libpebble/dataloggingendpoint.cpp new file mode 100644 index 0000000..a571c25 --- /dev/null +++ b/rockworkd/libpebble/dataloggingendpoint.cpp @@ -0,0 +1,44 @@ +#include "dataloggingendpoint.h" + +#include "pebble.h" +#include "watchconnection.h" +#include "watchdatareader.h" +#include "watchdatawriter.h" + +DataLoggingEndpoint::DataLoggingEndpoint(Pebble *pebble, WatchConnection *connection): + QObject(pebble), + m_pebble(pebble), + m_connection(connection) +{ + m_connection->registerEndpointHandler(WatchConnection::EndpointDataLogging, this, "handleMessage"); +} + +void DataLoggingEndpoint::handleMessage(const QByteArray &data) +{ + qDebug() << "data logged" << data.toHex(); + WatchDataReader reader(data); + DataLoggingCommand command = (DataLoggingCommand)reader.read<quint8>(); + switch (command) { + case DataLoggingDespoolSendData: { + quint8 sessionId = reader.read<quint8>(); + quint32 itemsLeft = reader.readLE<quint32>(); + quint32 crc = reader.readLE<quint32>(); + qDebug() << "Despooling data: Session:" << sessionId << "Items left:" << itemsLeft << "CRC:" << crc; + + QByteArray reply; + WatchDataWriter writer(&reply); + writer.write<quint8>(DataLoggingACK); + writer.write<quint8>(sessionId); + m_connection->writeToPebble(WatchConnection::EndpointDataLogging, reply); + return; + } + case DataLoggingTimeout: { + quint8 sessionId = reader.read<quint8>(); + qDebug() << "DataLogging reached timeout: Session:" << sessionId; + return; + } + default: + qDebug() << "Unhandled DataLogging message"; + } +} + diff --git a/rockworkd/libpebble/dataloggingendpoint.h b/rockworkd/libpebble/dataloggingendpoint.h new file mode 100644 index 0000000..2c5dfc5 --- /dev/null +++ b/rockworkd/libpebble/dataloggingendpoint.h @@ -0,0 +1,39 @@ +#ifndef DATALOGGINGENDPOINT_H +#define DATALOGGINGENDPOINT_H + +#include <QObject> + +class Pebble; +class WatchConnection; + +class DataLoggingEndpoint : public QObject +{ + Q_OBJECT +public: + enum DataLoggingCommand { + DataLoggingDespoolOpenSession = 0x01, + DataLoggingDespoolSendData = 0x02, + DataLoggingCloseSession = 0x03, + DataLoggingReportOpenSessions = 0x84, + DataLoggingACK = 0x85, + DataLoggingNACK = 0x86, + DataLoggingTimeout = 0x07, + DataLoggingEmptySession = 0x88, + DataLoggingGetSendEnableRequest = 0x89, + DataLoggingGetSendEnableResponse = 0x0A, + DataLoggingSetSendEnable = 0x8B + }; + + explicit DataLoggingEndpoint(Pebble *pebble, WatchConnection *connection); + +signals: + +private slots: + void handleMessage(const QByteArray &data); + +private: + Pebble *m_pebble; + WatchConnection *m_connection; +}; + +#endif // DATALOGGINGENDPOINT_H diff --git a/rockworkd/libpebble/enums.h b/rockworkd/libpebble/enums.h new file mode 100644 index 0000000..d6184c6 --- /dev/null +++ b/rockworkd/libpebble/enums.h @@ -0,0 +1,95 @@ +#ifndef ENUMS_H +#define ENUMS_H + +#include <QMetaType> + +enum HardwareRevision { + HardwareRevisionUNKNOWN = 0, + HardwareRevisionTINTIN_EV1 = 1, + HardwareRevisionTINTIN_EV2 = 2, + HardwareRevisionTINTIN_EV2_3 = 3, + HardwareRevisionTINTIN_EV2_4 = 4, + HardwareRevisionTINTIN_V1_5 = 5, + HardwareRevisionBIANCA = 6, + HardwareRevisionSNOWY_EVT2 = 7, + HardwareRevisionSNOWY_DVT = 8, + HardwareRevisionSPALDING_EVT = 9, + HardwareRevisionBOBBY_SMILES = 10, + HardwareRevisionSPALDING = 11, + + HardwareRevisionTINTIN_BB = 0xFF, + HardwareRevisionTINTIN_BB2 = 0xFE, + HardwareRevisionSNOWY_BB = 0xFD, + HardwareRevisionSNOWY_BB2 = 0xFC, + HardwareRevisionSPALDING_BB2 = 0xFB +}; + +enum OS { + OSUnknown = 0, + OSiOS = 1, + OSAndroid = 2, + OSOSX = 3, + OSLinux = 4, + OSWindows = 5 +}; + +enum HardwarePlatform { + HardwarePlatformUnknown = 0, + HardwarePlatformAplite, + HardwarePlatformBasalt, + HardwarePlatformChalk +}; + +enum Model { + ModelUnknown = 0, + ModelTintinBlack = 1, + ModelTintinWhite = 2, + ModelTintinRed = 3, + ModelTintinOrange = 4, + ModelTintinGrey = 5, + ModelBiancaSilver = 6, + ModelBiancaBlack = 7, + ModelTintinBlue = 8, + ModelTintinGreen = 9, + ModelTintinPink = 10, + ModelSnowyWhite = 11, + ModelSnowyBlack = 12, + ModelSnowyRed = 13, + ModelBobbySilver = 14, + ModelBobbyBlack = 15, + ModelBobbyGold = 16, + ModelSpalding14Silver = 17, + ModelSpalding14Black = 18, + ModelSpalding20Silver = 19, + ModelSpalding20Black = 20, + ModelSpalding14RoseGold = 21 +}; + +enum MusicControlButton { + MusicControlPlayPause, + MusicControlSkipBack, + MusicControlSkipNext, + MusicControlVolumeUp, + MusicControlVolumeDown +}; + +enum CallStatus { + CallStatusIncoming, + CallStatusOutGoing +}; + +enum Capability { + CapabilityNone = 0x0000000000000000, + CapabilityAppRunState = 0x0000000000000001, + CapabilityInfiniteLogDumping = 0x0000000000000002, + CapabilityUpdatedMusicProtocol = 0x0000000000000004, + CapabilityExtendedNotifications = 0x0000000000000008, + CapabilityLanguagePacks = 0x0000000000000010, + Capability8kAppMessages = 0x0000000000000020, + CapabilityHealth = 0x0000000000000040, + CapabilityVoice = 0x0000000000000080 +}; +Q_DECLARE_FLAGS(Capabilities, Capability) + +#endif // ENUMS_H + diff --git a/rockworkd/libpebble/firmwaredownloader.cpp b/rockworkd/libpebble/firmwaredownloader.cpp new file mode 100644 index 0000000..5d32f3b --- /dev/null +++ b/rockworkd/libpebble/firmwaredownloader.cpp @@ -0,0 +1,246 @@ +#include "firmwaredownloader.h" +#include "ziphelper.h" +#include "pebble.h" +#include "watchconnection.h" +#include "uploadmanager.h" + +#include <QNetworkAccessManager> +#include <QUrlQuery> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QJsonDocument> +#include <QFile> +#include <QDir> +#include <QCryptographicHash> + +FirmwareDownloader::FirmwareDownloader(Pebble *pebble, WatchConnection *connection): + QObject(pebble), + m_pebble(pebble), + m_connection(connection) +{ + m_nam = new QNetworkAccessManager(this); + + m_connection->registerEndpointHandler(WatchConnection::EndpointSystemMessage, this, "systemMessageReceived"); +} + +bool FirmwareDownloader::updateAvailable() const +{ + return m_updateAvailable; +} + +QString FirmwareDownloader::candidateVersion() const +{ + return m_candidateVersion; +} + +QString FirmwareDownloader::releaseNotes() const +{ + return m_releaseNotes; +} + +QString FirmwareDownloader::url() const +{ + return m_url; +} + +bool FirmwareDownloader::upgrading() const +{ + return m_upgradeInProgress; +} + +void FirmwareDownloader::performUpgrade() +{ + if (!m_updateAvailable) { + qWarning() << "No update available"; + return; + } + + if (m_upgradeInProgress) { + qWarning() << "Upgrade already in progress. Won't start another one"; + return; + } + + m_upgradeInProgress = true; + emit upgradingChanged(); + + QNetworkRequest request(m_url); + QNetworkReply *reply = m_nam->get(request); + connect(reply, &QNetworkReply::finished, [this, reply](){ + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Erorr fetching firmware" << reply->errorString(); + m_upgradeInProgress = false; + emit upgradingChanged(); + return; + } + + QByteArray data = reply->readAll(); + + QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex(); + + if (hash != m_hash) { + qWarning() << "Downloaded data hash doesn't match hash from target"; + m_upgradeInProgress = false; + emit upgradingChanged(); + return; + } + + QDir dir("/tmp/" + m_pebble->address().toString().replace(":", "_")); + if (!dir.exists() && !dir.mkpath(dir.absolutePath())) { + qWarning() << "Error saving file" << dir.absolutePath(); + m_upgradeInProgress = false; + emit upgradingChanged(); + return; + } + QString path = "/tmp/" + m_pebble->address().toString().replace(":", "_"); + QFile f(path + "/" + reply->request().url().fileName()); + if (!f.open(QFile::WriteOnly | QFile::Truncate)) { + qWarning() << "Cannot open tmp file for writing" << f.fileName(); + m_upgradeInProgress = false; + emit upgradingChanged(); + return; + } + f.write(data); + f.close(); + + if (!ZipHelper::unpackArchive(f.fileName(), path)) { + qWarning() << "Error unpacking firmware archive"; + m_upgradeInProgress = false; + emit upgradingChanged(); + return; + } + + Bundle firmware(path); + if (firmware.file(Bundle::FileTypeFirmware).isEmpty() || firmware.file(Bundle::FileTypeResources).isEmpty()) { + qWarning() << "Firmware bundle file missing binary or resources"; + m_upgradeInProgress = false; + emit upgradingChanged(); + return; + } + + qDebug() << "** Starting firmware upgrade **"; + m_bundlePath = path; + m_connection->systemMessage(WatchConnection::SystemMessageFirmwareStart); + + }); +} + +void FirmwareDownloader::checkForNewFirmware() +{ + QString platformString; + switch (m_pebble->hardwareRevision()) { + case HardwareRevisionUNKNOWN: + case HardwareRevisionTINTIN_EV1: + case HardwareRevisionTINTIN_EV2: + case HardwareRevisionTINTIN_EV2_3: + case HardwareRevisionSNOWY_EVT2: + case HardwareRevisionSPALDING_EVT: + case HardwareRevisionTINTIN_BB: + case HardwareRevisionTINTIN_BB2: + case HardwareRevisionSNOWY_BB: + case HardwareRevisionSNOWY_BB2: + case HardwareRevisionSPALDING_BB2: + qWarning() << "Hardware revision not supported for firmware upgrades" << m_pebble->hardwareRevision(); + return; + case HardwareRevisionTINTIN_EV2_4: + platformString = "ev2_4"; + break; + case HardwareRevisionTINTIN_V1_5: + platformString = "v1_5"; + break; + case HardwareRevisionBIANCA: + platformString = "v2_0"; + break; + case HardwareRevisionSNOWY_DVT: + platformString = "snowy_dvt"; + break; + case HardwareRevisionBOBBY_SMILES: + platformString = "snowy_s3"; + break; + case HardwareRevisionSPALDING: + platformString = "spalding"; + break; + + } + + QString url("https://pebblefw.s3.amazonaws.com/pebble/%1/%2/latest.json"); + url = url.arg(platformString).arg("release-v3.8"); + QNetworkRequest request(url); + QNetworkReply *reply = m_nam->get(request); + connect(reply, &QNetworkReply::finished, [this, reply]() { + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qWarning() << "Error parsing firmware fetch reply" << jsonDoc.toJson(QJsonDocument::Indented); + return; + } + QVariantMap resultMap = jsonDoc.toVariant().toMap(); + if (!resultMap.contains("normal")) { + qWarning() << "Could not find normal firmware package" << jsonDoc.toJson(QJsonDocument::Indented); + return; + } + + + QVariantMap targetFirmware; + if (resultMap.contains("3.x-migration") && m_pebble->softwareVersion() < "v3.0.0") { + targetFirmware = resultMap.value("3.x-migration").toMap(); + } else if (m_pebble->softwareVersion() >= "v3.0.0" && + resultMap.value("normal").toMap().value("friendlyVersion").toString() > m_pebble->softwareVersion()){ + targetFirmware = resultMap.value("normal").toMap(); + } + + if (targetFirmware.isEmpty()) { + qDebug() << "Watch firmware is up to date"; + m_updateAvailable = false; + emit updateAvailableChanged(); + return; + } + + qDebug() << targetFirmware; + + m_candidateVersion = targetFirmware.value("friendlyVersion").toString(); + m_releaseNotes = targetFirmware.value("notes").toString(); + m_url = targetFirmware.value("url").toString(); + m_hash = targetFirmware.value("sha-256").toByteArray(); + m_updateAvailable = true; + qDebug() << "candidate firmware upgrade" << m_candidateVersion << m_releaseNotes << m_url; + emit updateAvailableChanged(); + }); +} + +void FirmwareDownloader::systemMessageReceived(const QByteArray &data) +{ + qDebug() << "system message" << data.toHex(); + + if (!m_upgradeInProgress) { + return; + } + + Bundle firmware(m_bundlePath); + + qDebug() << "** Uploading firmware resources..."; + m_connection->uploadManager()->uploadFirmwareResources(firmware.file(Bundle::FileTypeResources), firmware.crc(Bundle::FileTypeResources), [this, firmware]() { + qDebug() << "** Firmware resources uploaded. OK"; + + qDebug() << "** Uploading firmware binary..."; + m_connection->uploadManager()->uploadFirmwareBinary(false, firmware.file(Bundle::FileTypeFirmware), firmware.crc(Bundle::FileTypeFirmware), [this]() { + qDebug() << "** Firmware binary uploaded. OK"; + m_connection->systemMessage(WatchConnection::SystemMessageFirmwareComplete); + m_upgradeInProgress = false; + emit upgradingChanged(); + }, [this](int code) { + qWarning() << "** ERROR uploading firmware binary" << code; + m_connection->systemMessage(WatchConnection::SystemMessageFirmwareFail); + m_upgradeInProgress = false; + emit upgradingChanged(); + }); + }, + [this](int code) { + qWarning() << "** ERROR uploading firmware resources" << code; + m_connection->systemMessage(WatchConnection::SystemMessageFirmwareFail); + m_upgradeInProgress = false; + emit upgradingChanged(); + }); +} + diff --git a/rockworkd/libpebble/firmwaredownloader.h b/rockworkd/libpebble/firmwaredownloader.h new file mode 100644 index 0000000..d7bd5b8 --- /dev/null +++ b/rockworkd/libpebble/firmwaredownloader.h @@ -0,0 +1,50 @@ +#ifndef FIRWAREDOWNLOADER_H +#define FIRWAREDOWNLOADER_H + +#include <QObject> + +#include "watchconnection.h" + +class Pebble; +class QNetworkAccessManager; + +class FirmwareDownloader : public QObject +{ + Q_OBJECT +public: + explicit FirmwareDownloader(Pebble *pebble, WatchConnection *connection); + + bool updateAvailable() const; + QString candidateVersion() const; + QString releaseNotes() const; + QString url() const; + + bool upgrading() const; + +public slots: + void checkForNewFirmware(); + void performUpgrade(); + +signals: + void updateAvailableChanged(); + void upgradingChanged(); + +private slots: + void systemMessageReceived(const QByteArray &data); + +private: + QNetworkAccessManager *m_nam; + Pebble *m_pebble; + WatchConnection *m_connection; + + bool m_updateAvailable = false; + QString m_candidateVersion; + QString m_releaseNotes; + QString m_url; + QByteArray m_hash; + + bool m_upgradeInProgress = false; + QString m_bundlePath; +}; + +#endif // FIRWAREDOWNLOADER_H diff --git a/rockworkd/libpebble/healthparams.cpp b/rockworkd/libpebble/healthparams.cpp new file mode 100644 index 0000000..270d950 --- /dev/null +++ b/rockworkd/libpebble/healthparams.cpp @@ -0,0 +1,93 @@ +#include "healthparams.h" + +#include "watchdatawriter.h" + +HealthParams::HealthParams() +{ + +} + +bool HealthParams::enabled() const +{ + return m_enabled; +} + +void HealthParams::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + +int HealthParams::height() const +{ + return m_height; +} + +void HealthParams::setHeight(int height) +{ + m_height = height; +} + +int HealthParams::weight() const +{ + return m_weight; +} + +void HealthParams::setWeight(int weight) +{ + m_weight = weight; +} + +bool HealthParams::moreActive() const +{ + return m_moreActive; +} + +void HealthParams::setMoreActive(bool moreActive) +{ + m_moreActive = moreActive; +} + +bool HealthParams::sleepMore() const +{ + return m_sleepMore; +} + +void HealthParams::setSleepMore(bool sleepMore) +{ + m_sleepMore = sleepMore; +} + +int HealthParams::age() const +{ + return m_age; +} + +void HealthParams::setAge(int age) +{ + m_age = age; +} + +HealthParams::Gender HealthParams::gender() const +{ + return m_gender; +} + +void HealthParams::setGender(HealthParams::Gender gender) +{ + m_gender = gender; +} + +QByteArray HealthParams::serialize() const +{ + QByteArray ret; + WatchDataWriter writer(&ret); + writer.writeLE<quint16>(m_height * 10); + writer.writeLE<quint16>(m_weight * 100); + writer.write<quint8>(m_enabled ? 0x01 : 0x00); + writer.write<quint8>(m_moreActive ? 0x01 : 0x00); + writer.write<quint8>(m_sleepMore ? 0x01 : 0x00); + writer.write<quint8>(m_age); + writer.write<quint8>(m_gender); + return ret; +} + diff --git a/rockworkd/libpebble/healthparams.h b/rockworkd/libpebble/healthparams.h new file mode 100644 index 0000000..03dbfc1 --- /dev/null +++ b/rockworkd/libpebble/healthparams.h @@ -0,0 +1,52 @@ +#ifndef HEALTHPARAMS_H +#define HEALTHPARAMS_H + +#include "watchconnection.h" + +class HealthParams: public PebblePacket +{ +public: + enum Gender { + GenderFemale = 0x00, + GenderMale = 0x01 + }; + + HealthParams(); + + bool enabled() const; + void setEnabled(bool enabled); + + // In cm + int height() const; + void setHeight(int height); + + // In kg + int weight() const; + void setWeight(int weight); + + bool moreActive() const; + void setMoreActive(bool moreActive); + + bool sleepMore() const; + void setSleepMore(bool sleepMore); + + int age() const; + void setAge(int age); + + Gender gender() const; + void setGender(Gender gender); + + QByteArray serialize() const; + +private: + bool m_enabled = false; + int m_height = 0; + int m_weight = 0; + bool m_moreActive = false; + bool m_sleepMore = false; + int m_age = 0; + Gender m_gender = Gender::GenderFemale; + +}; + +#endif // HEALTHPARAMS_H diff --git a/rockworkd/libpebble/jskit/cacheLocalStorage.js b/rockworkd/libpebble/jskit/cacheLocalStorage.js new file mode 100644 index 0000000..22588a9 --- /dev/null +++ b/rockworkd/libpebble/jskit/cacheLocalStorage.js @@ -0,0 +1,11 @@ +//Since we don't have JS 6 support, this hack will allow us to save changes to localStorage when using dot or square bracket notation + +for (var key in localStorage) { + _jskit.localstorage.setItem(key, localStorage.getItem(key)); +} + +for (var key in _jskit.localstorage.keys()) { + if (localStorage[key] === undefined) { + _jskit.localstorage.removeItem(key); + } +} diff --git a/rockworkd/libpebble/jskit/jsfiles.qrc b/rockworkd/libpebble/jskit/jsfiles.qrc new file mode 100644 index 0000000..4dfbc1d --- /dev/null +++ b/rockworkd/libpebble/jskit/jsfiles.qrc @@ -0,0 +1,7 @@ +<RCC> + <qresource prefix="/"> + <file>typedarray.js</file> + <file>jskitsetup.js</file> + <file>cacheLocalStorage.js</file> + </qresource> +</RCC> diff --git a/rockworkd/libpebble/jskit/jskitconsole.cpp b/rockworkd/libpebble/jskit/jskitconsole.cpp new file mode 100644 index 0000000..3d6c85c --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitconsole.cpp @@ -0,0 +1,29 @@ +#include <QDebug> + +#include "jskitconsole.h" + +JSKitConsole::JSKitConsole(QObject *parent) : + QObject(parent), + l(metaObject()->className()) +{ +} + +void JSKitConsole::log(const QString &msg) +{ + qCDebug(l) << msg; +} + +void JSKitConsole::warn(const QString &msg) +{ + qCWarning(l) << msg; +} + +void JSKitConsole::error(const QString &msg) +{ + qCCritical(l) << msg; +} + +void JSKitConsole::info(const QString &msg) +{ + qCDebug(l) << msg; +} diff --git a/rockworkd/libpebble/jskit/jskitconsole.h b/rockworkd/libpebble/jskit/jskitconsole.h new file mode 100644 index 0000000..3896ae3 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitconsole.h @@ -0,0 +1,20 @@ +#ifndef JSKITCONSOLE_H +#define JSKITCONSOLE_H + +#include <QLoggingCategory> + +class JSKitConsole : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitConsole(QObject *parent=0); + + Q_INVOKABLE void log(const QString &msg); + Q_INVOKABLE void warn(const QString &msg); + Q_INVOKABLE void error(const QString &msg); + Q_INVOKABLE void info(const QString &msg); +}; + +#endif // JSKITCONSOLE_H diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.cpp b/rockworkd/libpebble/jskit/jskitgeolocation.cpp new file mode 100644 index 0000000..409cda1 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitgeolocation.cpp @@ -0,0 +1,302 @@ +#include <limits> + +#include "jskitgeolocation.h" + +JSKitGeolocation::JSKitGeolocation(QJSEngine *engine) : + QObject(engine), + l(metaObject()->className()), + m_engine(engine), + m_source(0), + m_lastWatcherId(0) +{ +} + +void JSKitGeolocation::getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options) +{ + setupWatcher(successCallback, errorCallback, options, true); +} + +int JSKitGeolocation::watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options) +{ + return setupWatcher(successCallback, errorCallback, options, false); +} + +void JSKitGeolocation::clearWatch(int watcherId) +{ + removeWatcher(watcherId); +} + +void JSKitGeolocation::handleError(QGeoPositionInfoSource::Error error) +{ + qCWarning(l) << "positioning error: " << error; + + if (m_watchers.empty()) { + qCWarning(l) << "got position error but no one is watching"; + stopAndRemove(); + } + else { + QJSValue obj; + if (error == QGeoPositionInfoSource::AccessError) { + obj = buildPositionErrorObject(PERMISSION_DENIED, "permission denied"); + } else { + obj = buildPositionErrorObject(POSITION_UNAVAILABLE, "position unavailable"); + } + + for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) { + invokeCallback(it->errorCallback, obj); + + if (it->once) { + it = m_watchers.erase(it); + } else { + it->timer.restart(); + ++it; + } + } + } +} + +void JSKitGeolocation::handlePosition(const QGeoPositionInfo &pos) +{ + qCDebug(l) << "got position at" << pos.timestamp() << "type" << pos.coordinate().type(); + + if (m_watchers.empty()) { + qCWarning(l) << "got position update but no one is watching"; + stopAndRemove(); + } + else { + QJSValue obj = buildPositionObject(pos); + + for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) { + invokeCallback(it->successCallback, obj); + + if (it->once) { + it = m_watchers.erase(it); + } else { + it->timer.restart(); + ++it; + } + } + } +} + +void JSKitGeolocation::handleTimeout() +{ + qCDebug(l) << "positioning timeout"; + + if (m_watchers.empty()) { + qCWarning(l) << "got position timeout but no one is watching"; + stopAndRemove(); + } + else { + QJSValue obj = buildPositionErrorObject(TIMEOUT, "timeout"); + + for (auto it = m_watchers.begin(); it != m_watchers.end(); /*no adv*/) { + if (it->timer.hasExpired(it->timeout)) { + qCDebug(l) << "positioning timeout for watch" << it->watcherId + << ", watch is" << it->timer.elapsed() << "ms old, timeout is" << it->timeout; + invokeCallback(it->errorCallback, obj); + + if (it->once) { + it = m_watchers.erase(it); + } else { + it->timer.restart(); + ++it; + } + } else { + ++it; + } + } + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); + } +} + +void JSKitGeolocation::updateTimeouts() +{ + int once_timeout = -1, updates_timeout = -1; + + Q_FOREACH(const Watcher &watcher, m_watchers) { + qint64 rem_timeout = watcher.timeout - watcher.timer.elapsed(); + qCDebug(l) << "watch" << watcher.watcherId << "rem timeout" << rem_timeout; + + if (rem_timeout >= 0) { + // Make sure the limits aren't too large + rem_timeout = qMin<qint64>(rem_timeout, std::numeric_limits<int>::max()); + + if (watcher.once) { + once_timeout = once_timeout >= 0 ? qMin<int>(once_timeout, rem_timeout) : rem_timeout; + } else { + updates_timeout = updates_timeout >= 0 ? qMin<int>(updates_timeout, rem_timeout) : rem_timeout; + } + } + } + + if (updates_timeout >= 0) { + qCDebug(l) << "setting location update interval to" << updates_timeout; + m_source->setUpdateInterval(updates_timeout); + m_source->startUpdates(); + } else { + qCDebug(l) << "stopping updates"; + m_source->stopUpdates(); + } + + if (once_timeout >= 0) { + qCDebug(l) << "requesting single location update with timeout" << once_timeout; + m_source->requestUpdate(once_timeout); + } + + if (once_timeout == 0 && updates_timeout == 0) { + stopAndRemove(); + } +} + +int JSKitGeolocation::setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once) +{ + Watcher watcher; + watcher.successCallback = successCallback; + watcher.errorCallback = errorCallback; + watcher.highAccuracy = options.value("enableHighAccuracy", false).toBool(); + watcher.timeout = options.value("timeout", std::numeric_limits<int>::max() - 1).toInt(); + watcher.maximumAge = options.value("maximumAge", 0).toLongLong(); + watcher.once = once; + watcher.watcherId = ++m_lastWatcherId; + + qCDebug(l) << "setting up watcher, gps=" << watcher.highAccuracy << "timeout=" << watcher.timeout << "maximumAge=" << watcher.maximumAge << "once=" << watcher.once; + + if (!m_source) { + m_source = QGeoPositionInfoSource::createDefaultSource(this); + + connect(m_source, static_cast<void (QGeoPositionInfoSource::*)(QGeoPositionInfoSource::Error)>(&QGeoPositionInfoSource::error), + this, &JSKitGeolocation::handleError); + connect(m_source, &QGeoPositionInfoSource::positionUpdated, + this, &JSKitGeolocation::handlePosition); + connect(m_source, &QGeoPositionInfoSource::updateTimeout, + this, &JSKitGeolocation::handleTimeout); + } + + if (watcher.maximumAge > 0) { + QDateTime threshold = QDateTime::currentDateTime().addMSecs(-qint64(watcher.maximumAge)); + QGeoPositionInfo pos = m_source->lastKnownPosition(watcher.highAccuracy); + qCDebug(l) << "got pos timestamp" << pos.timestamp() << " but we want" << threshold; + + if (pos.isValid() && pos.timestamp() >= threshold) { + invokeCallback(watcher.successCallback, buildPositionObject(pos)); + + if (once) { + return -1; + } + } else if (watcher.timeout == 0 && once) { + // If the timeout has already expired, and we have no cached data + // Do not even bother to turn on the GPS; return error object now. + invokeCallback(watcher.errorCallback, buildPositionErrorObject(TIMEOUT, "no cached position")); + return -1; + } + } + + watcher.timer.start(); + m_watchers.append(watcher); + + qCDebug(l) << "added new watcher" << watcher.watcherId; + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); + + return watcher.watcherId; +} + +void JSKitGeolocation::removeWatcher(int watcherId) +{ + Watcher watcher; + + qCDebug(l) << "removing watcherId" << watcher.watcherId; + + for (int i = 0; i < m_watchers.size(); i++) { + if (m_watchers[i].watcherId == watcherId) { + watcher = m_watchers.takeAt(i); + break; + } + } + + if (watcher.watcherId != watcherId) { + qCWarning(l) << "watcherId not found"; + return; + } + + QMetaObject::invokeMethod(this, "updateTimeouts", Qt::QueuedConnection); +} + +QJSValue JSKitGeolocation::buildPositionObject(const QGeoPositionInfo &pos) +{ + QJSValue obj = m_engine->newObject(); + QJSValue coords = m_engine->newObject(); + QJSValue timestamp = m_engine->toScriptValue<quint64>(pos.timestamp().toMSecsSinceEpoch()); + + coords.setProperty("latitude", m_engine->toScriptValue(pos.coordinate().latitude())); + coords.setProperty("longitude", m_engine->toScriptValue(pos.coordinate().longitude())); + if (pos.coordinate().type() == QGeoCoordinate::Coordinate3D) { + coords.setProperty("altitude", m_engine->toScriptValue(pos.coordinate().altitude())); + } else { + coords.setProperty("altitude", m_engine->toScriptValue<void*>(0)); + } + + coords.setProperty("accuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::HorizontalAccuracy))); + + if (pos.hasAttribute(QGeoPositionInfo::VerticalAccuracy)) { + coords.setProperty("altitudeAccuracy", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::VerticalAccuracy))); + } else { + coords.setProperty("altitudeAccuracy", m_engine->toScriptValue<void*>(0)); + } + + if (pos.hasAttribute(QGeoPositionInfo::Direction)) { + coords.setProperty("heading", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::Direction))); + } else { + coords.setProperty("heading", m_engine->toScriptValue<void*>(0)); + } + + if (pos.hasAttribute(QGeoPositionInfo::GroundSpeed)) { + coords.setProperty("speed", m_engine->toScriptValue(pos.attribute(QGeoPositionInfo::GroundSpeed))); + } else { + coords.setProperty("speed", m_engine->toScriptValue<void*>(0)); + } + + obj.setProperty("coords", coords); + obj.setProperty("timestamp", timestamp); + + return obj; +} + +QJSValue JSKitGeolocation::buildPositionErrorObject(PositionError error, const QString &message) +{ + QJSValue obj = m_engine->newObject(); + + obj.setProperty("code", m_engine->toScriptValue<unsigned short>(error)); + obj.setProperty("message", m_engine->toScriptValue(message)); + + return obj; +} + +void JSKitGeolocation::invokeCallback(QJSValue callback, QJSValue event) +{ + if (callback.isCallable()) { + qCDebug(l) << "invoking callback" << callback.toString(); + QJSValue result = callback.call(QJSValueList({event})); + + if (result.isError()) { + qCWarning(l) << "error while invoking callback: " << QString("%1:%2: %3") + .arg(result.property("fileName").toString()) + .arg(result.property("lineNumber").toInt()) + .arg(result.toString()); + } + } else { + qCWarning(l) << "callback is not callable"; + } +} + +void JSKitGeolocation::stopAndRemove() +{ + if (m_source) { + qCDebug(l) << "removing source"; + + m_source->stopUpdates(); + m_source->deleteLater(); + m_source = 0; + } +} diff --git a/rockworkd/libpebble/jskit/jskitgeolocation.h b/rockworkd/libpebble/jskit/jskitgeolocation.h new file mode 100644 index 0000000..582ab32 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitgeolocation.h @@ -0,0 +1,66 @@ +#ifndef JSKITGEOLOCATION_H +#define JSKITGEOLOCATION_H + +#include <QElapsedTimer> +#include <QGeoPositionInfoSource> +#include <QJSValue> +#include <QLoggingCategory> +#include <QJSEngine> + +class JSKitGeolocation : public QObject +{ + Q_OBJECT + QLoggingCategory l; + + struct Watcher; + +public: + explicit JSKitGeolocation(QJSEngine *engine); + + enum PositionError { + PERMISSION_DENIED = 1, + POSITION_UNAVAILABLE = 2, + TIMEOUT = 3 + }; + Q_ENUMS(PositionError); + + Q_INVOKABLE void getCurrentPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap()); + Q_INVOKABLE int watchPosition(const QJSValue &successCallback, const QJSValue &errorCallback = QJSValue(), const QVariantMap &options = QVariantMap()); + Q_INVOKABLE void clearWatch(int watcherId); + +private slots: + void handleError(const QGeoPositionInfoSource::Error error); + void handlePosition(const QGeoPositionInfo &pos); + void handleTimeout(); + void updateTimeouts(); + +private: + int setupWatcher(const QJSValue &successCallback, const QJSValue &errorCallback, const QVariantMap &options, bool once); + void removeWatcher(int watcherId); + + QJSValue buildPositionObject(const QGeoPositionInfo &pos); + QJSValue buildPositionErrorObject(PositionError error, const QString &message = QString()); + QJSValue buildPositionErrorObject(const QGeoPositionInfoSource::Error error); + void invokeCallback(QJSValue callback, QJSValue event); + void stopAndRemove(); + +private: + QJSEngine *m_engine; + QGeoPositionInfoSource *m_source; + + struct Watcher { + QJSValue successCallback; + QJSValue errorCallback; + int watcherId; + bool once; + bool highAccuracy; + int timeout; + QElapsedTimer timer; + qlonglong maximumAge; + }; + + QList<Watcher> m_watchers; + int m_lastWatcherId; +}; + +#endif // JSKITGEOLOCATION_H diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.cpp b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp new file mode 100644 index 0000000..d69b6ad --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitlocalstorage.cpp @@ -0,0 +1,117 @@ +#include <QDesktopServices> +#include <QDir> +#include <QDebug> + +#include "jskitlocalstorage.h" + +JSKitLocalStorage::JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid): + QObject(engine), + m_engine(engine), + m_storage(new QSettings(getStorageFileFor(storagePath, uuid), QSettings::IniFormat, this)) +{ +} + +int JSKitLocalStorage::length() const +{ + return m_storage->allKeys().size(); +} + +QJSValue JSKitLocalStorage::getItem(const QJSValue &key) const +{ + QVariant value = m_storage->value(key.toString()); + + if (value.isValid()) { + return QJSValue(value.toString()); + } else { + return QJSValue(QJSValue::NullValue); + } +} + +bool JSKitLocalStorage::setItem(const QJSValue &key, const QJSValue &value) +{ + m_storage->setValue(key.toString(), QVariant::fromValue(value.toString())); + return true; +} + +bool JSKitLocalStorage::removeItem(const QJSValue &key) +{ + if (m_storage->contains(key.toString())) { + m_storage->remove(key.toString()); + return true; + } else { + return false; + } +} + +void JSKitLocalStorage::clear() +{ + m_storage->clear(); +} + +QJSValue JSKitLocalStorage::key(int index) +{ + QStringList allKeys = m_storage->allKeys(); + QJSValue key(QJSValue::NullValue); + + if (allKeys.size() > index) { + key = QJSValue(allKeys[index]); + } + + return key; +} + +QJSValue JSKitLocalStorage::get(const QJSValue &proxy, const QJSValue &key) const +{ + Q_UNUSED(proxy); + return getItem(key); +} + +bool JSKitLocalStorage::set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value) +{ + Q_UNUSED(proxy); + return setItem(key, value); +} + +bool JSKitLocalStorage::has(const QJSValue &proxy, const QJSValue &key) +{ + Q_UNUSED(proxy); + return m_storage->contains(key.toString()); +} + +bool JSKitLocalStorage::deleteProperty(const QJSValue &proxy, const QJSValue &key) +{ + Q_UNUSED(proxy); + return removeItem(key); +} + +QJSValue JSKitLocalStorage::keys(const QJSValue &proxy) +{ + Q_UNUSED(proxy); + + QStringList allKeys = m_storage->allKeys(); + QJSValue keyArray = m_engine->newArray(allKeys.size()); + for (int i = 0; i < allKeys.size(); i++) { + keyArray.setProperty(i, allKeys[i]); + } + + return keyArray; +} + +QJSValue JSKitLocalStorage::enumerate() +{ + return keys(0); +} + +QString JSKitLocalStorage::getStorageFileFor(const QString &storageDir, const QUuid &uuid) +{ + QDir dataDir(storageDir + "/js-storage"); + if (!dataDir.exists() && !dataDir.mkpath(dataDir.absolutePath())) { + qWarning() << "Error creating jskit storage dir"; + return QString(); + } + + QString fileName = uuid.toString(); + fileName.remove('{'); + fileName.remove('}'); + return dataDir.absoluteFilePath(fileName + ".ini"); +} diff --git a/rockworkd/libpebble/jskit/jskitlocalstorage.h b/rockworkd/libpebble/jskit/jskitlocalstorage.h new file mode 100644 index 0000000..9719f83 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitlocalstorage.h @@ -0,0 +1,40 @@ +#ifndef JSKITLOCALSTORAGE_P_H +#define JSKITLOCALSTORAGE_P_H + +#include <QSettings> +#include <QJSEngine> +#include <QUuid> + +class JSKitLocalStorage : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int length READ length) + +public: + explicit JSKitLocalStorage(QJSEngine *engine, const QString &storagePath, const QUuid &uuid); + + int length() const; + + Q_INVOKABLE QJSValue getItem(const QJSValue &key) const; + Q_INVOKABLE bool setItem(const QJSValue &key, const QJSValue &value); + Q_INVOKABLE bool removeItem(const QJSValue &key); + Q_INVOKABLE void clear(); + Q_INVOKABLE QJSValue key(int index); + + Q_INVOKABLE QJSValue get(const QJSValue &proxy, const QJSValue &key) const; + Q_INVOKABLE bool set(const QJSValue &proxy, const QJSValue &key, const QJSValue &value); + Q_INVOKABLE bool has(const QJSValue &proxy, const QJSValue &key); + Q_INVOKABLE bool deleteProperty(const QJSValue &proxy, const QJSValue &key); + Q_INVOKABLE QJSValue keys(const QJSValue &proxy=0); + Q_INVOKABLE QJSValue enumerate(); + +private: + static QString getStorageFileFor(const QString &storageDir, const QUuid &uuid); + +private: + QJSEngine *m_engine; + QSettings *m_storage; +}; + +#endif // JSKITLOCALSTORAGE_P_H diff --git a/rockworkd/libpebble/jskit/jskitmanager.cpp b/rockworkd/libpebble/jskit/jskitmanager.cpp new file mode 100644 index 0000000..04bf674 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitmanager.cpp @@ -0,0 +1,240 @@ +#include <QFile> +#include <QDir> +#include <QUrl> + +#include "jskitmanager.h" +#include "jskitpebble.h" + +JSKitManager::JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent) : + QObject(parent), + l(metaObject()->className()), + m_pebble(pebble), + m_connection(connection), + m_apps(apps), + m_appmsg(appmsg), + m_engine(0), + m_configurationUuid(0) +{ + connect(m_appmsg, &AppMsgManager::appStarted, this, &JSKitManager::handleAppStarted); + connect(m_appmsg, &AppMsgManager::appStopped, this, &JSKitManager::handleAppStopped); +} + +JSKitManager::~JSKitManager() +{ + if (m_engine) { + stopJsApp(); + } +} + +QJSEngine * JSKitManager::engine() +{ + return m_engine; +} + +bool JSKitManager::isJSKitAppRunning() const +{ + return m_engine != 0; +} + +QString JSKitManager::describeError(QJSValue error) +{ + return QString("%1:%2: %3") + .arg(error.property("fileName").toString()) + .arg(error.property("lineNumber").toInt()) + .arg(error.toString()); +} + +void JSKitManager::showConfiguration() +{ + if (m_engine) { + qCDebug(l) << "requesting configuration"; + m_jspebble->invokeCallbacks("showConfiguration"); + } else { + qCWarning(l) << "requested to show configuration, but JS engine is not running"; + } +} + +void JSKitManager::handleWebviewClosed(const QString &result) +{ + if (m_engine) { + QJSValue eventObj = m_engine->newObject(); + eventObj.setProperty("response", QUrl::fromPercentEncoding(result.toUtf8())); + + qCDebug(l) << "Sending" << eventObj.property("response").toString(); + m_jspebble->invokeCallbacks("webviewclosed", QJSValueList({eventObj})); + + loadJsFile(":/cacheLocalStorage.js"); + } else { + qCWarning(l) << "webview closed event, but JS engine is not running"; + } +} + +void JSKitManager::setConfigurationId(const QUuid &uuid) +{ + m_configurationUuid = uuid; +} + +AppInfo JSKitManager::currentApp() +{ + return m_curApp; +} + +void JSKitManager::handleAppStarted(const QUuid &uuid) +{ + AppInfo info = m_apps->info(uuid); + if (!info.uuid().isNull() && info.isJSKit()) { + qCDebug(l) << "Preparing to start JSKit app" << info.uuid() << info.shortName(); + + m_curApp = info; + startJsApp(); + } +} + +void JSKitManager::handleAppStopped(const QUuid &uuid) +{ + if (!m_curApp.uuid().isNull()) { + if (m_curApp.uuid() != uuid) { + qCWarning(l) << "Closed app with invalid UUID"; + } + + stopJsApp(); + m_curApp = AppInfo(); + qCDebug(l) << "App stopped" << uuid; + } +} + +void JSKitManager::handleAppMessage(const QUuid &uuid, const QVariantMap &msg) +{ + if (m_curApp.uuid() == uuid) { + qCDebug(l) << "handling app message" << uuid << msg; + + if (m_engine) { + QJSValue eventObj = m_engine->newObject(); + eventObj.setProperty("payload", m_engine->toScriptValue(msg)); + + m_jspebble->invokeCallbacks("appmessage", QJSValueList({eventObj})); + + loadJsFile(":/cacheLocalStorage.js"); + } + else { + qCDebug(l) << "but engine is stopped"; + } + } +} + +bool JSKitManager::loadJsFile(const QString &filename) +{ + Q_ASSERT(m_engine); + + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(l) << "Failed to load JS file:" << file.fileName(); + return false; + } + + qCDebug(l) << "evaluating js file" << file.fileName(); + + QJSValue result = m_engine->evaluate(QString::fromUtf8(file.readAll()), file.fileName()); + if (result.isError()) { + qCWarning(l) << "error while evaluating JS script:" << describeError(result); + return false; + } + + qCDebug(l) << "JS script evaluated"; + return true; +} + +void JSKitManager::startJsApp() +{ + if (m_engine) stopJsApp(); + + if (m_curApp.uuid().isNull()) { + qCWarning(l) << "Attempting to start JS app with invalid UUID"; + return; + } + + m_engine = new QJSEngine(this); + m_jspebble = new JSKitPebble(m_curApp, this, m_engine); + m_jsconsole = new JSKitConsole(m_engine); + m_jsstorage = new JSKitLocalStorage(m_engine, m_pebble->storagePath(), m_curApp.uuid()); + m_jsgeo = new JSKitGeolocation(m_engine); + m_jstimer = new JSKitTimer(m_engine); + m_jsperformance = new JSKitPerformance(m_engine); + + qCDebug(l) << "starting JS app" << m_curApp.shortName(); + + QJSValue globalObj = m_engine->globalObject(); + QJSValue jskitObj = m_engine->newObject(); + + jskitObj.setProperty("pebble", m_engine->newQObject(m_jspebble)); + jskitObj.setProperty("console", m_engine->newQObject(m_jsconsole)); + jskitObj.setProperty("localstorage", m_engine->newQObject(m_jsstorage)); + jskitObj.setProperty("geolocation", m_engine->newQObject(m_jsgeo)); + jskitObj.setProperty("timer", m_engine->newQObject(m_jstimer)); + jskitObj.setProperty("performance", m_engine->newQObject(m_jsperformance)); + globalObj.setProperty("_jskit", jskitObj); + + QJSValue navigatorObj = m_engine->newObject(); + navigatorObj.setProperty("language", m_engine->toScriptValue(QLocale().name())); + globalObj.setProperty("navigator", navigatorObj); + + // Set this.window = this + globalObj.setProperty("window", globalObj); + + // Shims for compatibility... + loadJsFile(":/jskitsetup.js"); + + // Polyfills... + loadJsFile(":/typedarray.js"); + + // Now the actual script + QString jsApp = m_curApp.file(AppInfo::FileTypeJsApp, HardwarePlatformUnknown); + QFile f(jsApp); + if (!f.open(QFile::ReadOnly)) { + qCWarning(l) << "Error opening" << jsApp; + return; + } + QJSValue ret = m_engine->evaluate(QString::fromUtf8(f.readAll())); + qCDebug(l) << "loaded script" << ret.toString(); + + // Setup the message callback + QUuid uuid = m_curApp.uuid(); + m_appmsg->setMessageHandler(uuid, [this, uuid](const QVariantMap &msg) { + QMetaObject::invokeMethod(this, "handleAppMessage", Qt::QueuedConnection, + Q_ARG(QUuid, uuid), + Q_ARG(QVariantMap, msg)); + + // Invoke the slot as a queued connection to give time for the ACK message + // to go through first. + + return true; + }); + + // We try to invoke the callbacks even if script parsing resulted in error... + m_jspebble->invokeCallbacks("ready"); + + loadJsFile(":/cacheLocalStorage.js"); + + if (m_configurationUuid == m_curApp.uuid()) { + qCDebug(l) << "going to launch config for" << m_configurationUuid; + showConfiguration(); + } + + m_configurationUuid = QUuid(); +} + +void JSKitManager::stopJsApp() +{ + qCDebug(l) << "stop js app" << m_curApp.uuid(); + if (!m_engine) return; // Nothing to do! + + loadJsFile(":/cacheLocalStorage.js"); + + if (!m_curApp.uuid().isNull()) { + m_appmsg->clearMessageHandler(m_curApp.uuid()); + } + + m_engine->collectGarbage(); + m_engine->deleteLater(); + m_engine = 0; +} diff --git a/rockworkd/libpebble/jskit/jskitmanager.h b/rockworkd/libpebble/jskit/jskitmanager.h new file mode 100644 index 0000000..570948e --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitmanager.h @@ -0,0 +1,72 @@ +#ifndef JSKITMANAGER_H +#define JSKITMANAGER_H + +#include <QJSEngine> +#include <QPointer> +#include <QLoggingCategory> + +#include "../appmanager.h" +#include "../watchconnection.h" +#include "../pebble.h" +#include "../appmsgmanager.h" + +#include "jskitconsole.h" +#include "jskitgeolocation.h" +#include "jskitlocalstorage.h" +#include "jskittimer.h" +#include "jskitperformance.h" + +class JSKitPebble; + +class JSKitManager : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitManager(Pebble *pebble, WatchConnection *connection, AppManager *apps, AppMsgManager *appmsg, QObject *parent = 0); + ~JSKitManager(); + + QJSEngine * engine(); + bool isJSKitAppRunning() const; + + static QString describeError(QJSValue error); + + void showConfiguration(); + void handleWebviewClosed(const QString &result); + void setConfigurationId(const QUuid &uuid); + AppInfo currentApp(); + +signals: + void appNotification(const QUuid &uuid, const QString &title, const QString &body); + void openURL(const QString &uuid, const QString &url); + +private slots: + void handleAppStarted(const QUuid &uuid); + void handleAppStopped(const QUuid &uuid); + void handleAppMessage(const QUuid &uuid, const QVariantMap &msg); + +private: + bool loadJsFile(const QString &filename); + void startJsApp(); + void stopJsApp(); + +private: + friend class JSKitPebble; + + Pebble *m_pebble; + WatchConnection *m_connection; + AppManager *m_apps; + AppMsgManager *m_appmsg; + AppInfo m_curApp; + QJSEngine *m_engine; + QPointer<JSKitPebble> m_jspebble; + QPointer<JSKitConsole> m_jsconsole; + QPointer<JSKitLocalStorage> m_jsstorage; + QPointer<JSKitGeolocation> m_jsgeo; + QPointer<JSKitTimer> m_jstimer; + QPointer<JSKitPerformance> m_jsperformance; + QUuid m_configurationUuid; +}; + +#endif // JSKITMANAGER_H diff --git a/rockworkd/libpebble/jskit/jskitpebble.cpp b/rockworkd/libpebble/jskit/jskitpebble.cpp new file mode 100644 index 0000000..a300aef --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitpebble.cpp @@ -0,0 +1,355 @@ +#include <QUrl> +#include <QCryptographicHash> +#include <QSettings> + +#include "jskitpebble.h" +#include "jskitxmlhttprequest.h" + +static const char *token_salt = "0feeb7416d3c4546a19b04bccd8419b1"; + +JSKitPebble::JSKitPebble(const AppInfo &info, JSKitManager *mgr, QObject *parent) : + QObject(parent), + l(metaObject()->className()), + m_appInfo(info), + m_mgr(mgr) +{ +} + +void JSKitPebble::addEventListener(const QString &type, QJSValue function) +{ + m_listeners[type].append(function); +} + +void JSKitPebble::removeEventListener(const QString &type, QJSValue function) +{ + if (!m_listeners.contains(type)) return; + + QList<QJSValue> &callbacks = m_listeners[type]; + for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ) { + if (it->strictlyEquals(function)) { + it = callbacks.erase(it); + } else { + ++it; + } + } + + if (callbacks.empty()) { + m_listeners.remove(type); + } +} + +void JSKitPebble::showSimpleNotificationOnPebble(const QString &title, const QString &body) +{ + qCDebug(l) << "showSimpleNotificationOnPebble" << title << body; + emit m_mgr->appNotification(m_appInfo.uuid(), title, body); +} + +uint JSKitPebble::sendAppMessage(QJSValue message, QJSValue callbackForAck, QJSValue callbackForNack) +{ + QVariantMap data = message.toVariant().toMap(); + QPointer<JSKitPebble> pebbObj = this; + uint transactionId = m_mgr->m_appmsg->nextTransactionId(); + + qCDebug(l) << "sendAppMessage" << data; + + m_mgr->m_appmsg->send( + m_appInfo.uuid(), + data, + [this, pebbObj, transactionId, callbackForAck]() mutable { + if (pebbObj.isNull()) return; + + if (callbackForAck.isCallable()) { + QJSValue event = pebbObj->buildAckEventObject(transactionId); + QJSValue result = callbackForAck.call(QJSValueList({event})); + + if (result.isError()) { + qCWarning(l) << "error while invoking ACK callback" + << callbackForAck.toString() << ":" + << JSKitManager::describeError(result); + } + } + }, + [this, pebbObj, transactionId, callbackForNack]() mutable { + if (pebbObj.isNull()) return; + + if (callbackForNack.isCallable()) { + QJSValue event = pebbObj->buildAckEventObject(transactionId, "NACK from watch"); + QJSValue result = callbackForNack.call(QJSValueList({event})); + + if (result.isError()) { + qCWarning(l) << "error while invoking NACK callback" + << callbackForNack.toString() << ":" + << JSKitManager::describeError(result); + } + } + } + ); + + return transactionId; +} + +void JSKitPebble::getTimelineToken(QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.getTimelineToken"; + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + +void JSKitPebble::timelineSubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.timelineSubscribe"; + Q_UNUSED(topic); + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + +void JSKitPebble::timelineUnsubscribe(const QString &topic, QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.timelineUnsubscribe"; + Q_UNUSED(topic); + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + +void JSKitPebble::timelineSubscriptions(QJSValue successCallback, QJSValue failureCallback) +{ + //TODO actually implement this + qCDebug(l) << "call to unsupported method Pebble.timelineSubscriptions"; + Q_UNUSED(successCallback); + + if (failureCallback.isCallable()) { + failureCallback.call(); + } +} + + +QString JSKitPebble::getAccountToken() const +{ + // We do not have any account system, so we just fake something up. + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(token_salt, strlen(token_salt)); + hasher.addData(m_appInfo.uuid().toByteArray()); + + QSettings settings; + QString token = settings.value("accountToken").toString(); + + if (token.isEmpty()) { + token = QUuid::createUuid().toString(); + qCDebug(l) << "created new account token" << token; + settings.setValue("accountToken", token); + } + + hasher.addData(token.toLatin1()); + + QString hash = hasher.result().toHex(); + qCDebug(l) << "returning account token" << hash; + + return hash; +} + +QString JSKitPebble::getWatchToken() const +{ + QCryptographicHash hasher(QCryptographicHash::Md5); + + hasher.addData(token_salt, strlen(token_salt)); + hasher.addData(m_appInfo.uuid().toByteArray()); + hasher.addData(m_mgr->m_pebble->serialNumber().toLatin1()); + + QString hash = hasher.result().toHex(); + qCDebug(l) << "returning watch token" << hash; + + return hash; +} + +QJSValue JSKitPebble::getActiveWatchInfo() const +{ + QJSValue watchInfo = m_mgr->m_engine->newObject(); + + switch (m_mgr->m_pebble->hardwarePlatform()) { + case HardwarePlatformBasalt: + watchInfo.setProperty("platform", "basalt"); + break; + + case HardwarePlatformChalk: + watchInfo.setProperty("platform", "chalk"); + break; + + default: + watchInfo.setProperty("platform", "aplite"); + break; + } + + switch (m_mgr->m_pebble->model()) { + case ModelTintinWhite: + watchInfo.setProperty("model", "pebble_white"); + break; + + case ModelTintinRed: + watchInfo.setProperty("model", "pebble_red"); + break; + + case ModelTintinOrange: + watchInfo.setProperty("model", "pebble_orange"); + break; + + case ModelTintinGrey: + watchInfo.setProperty("model", "pebble_grey"); + break; + + case ModelBiancaSilver: + watchInfo.setProperty("model", "pebble_steel_silver"); + break; + + case ModelBiancaBlack: + watchInfo.setProperty("model", "pebble_steel_black"); + break; + + case ModelTintinBlue: + watchInfo.setProperty("model", "pebble_blue"); + break; + + case ModelTintinGreen: + watchInfo.setProperty("model", "pebble_green"); + break; + + case ModelTintinPink: + watchInfo.setProperty("model", "pebble_pink"); + break; + + case ModelSnowyWhite: + watchInfo.setProperty("model", "pebble_time_white"); + break; + + case ModelSnowyBlack: + watchInfo.setProperty("model", "pebble_time_black"); + break; + + case ModelSnowyRed: + watchInfo.setProperty("model", "pebble_time_read"); + break; + + case ModelBobbySilver: + watchInfo.setProperty("model", "pebble_time_steel_silver"); + break; + + case ModelBobbyBlack: + watchInfo.setProperty("model", "pebble_time_steel_black"); + break; + + case ModelBobbyGold: + watchInfo.setProperty("model", "pebble_time_steel_gold"); + break; + + case ModelSpalding14Silver: + watchInfo.setProperty("model", "pebble_time_round_silver_14mm"); + break; + + case ModelSpalding14Black: + watchInfo.setProperty("model", "pebble_time_round_black_14mm"); + break; + + case ModelSpalding20Silver: + watchInfo.setProperty("model", "pebble_time_round_silver_20mm"); + break; + + case ModelSpalding20Black: + watchInfo.setProperty("model", "pebble_time_round_black_20mm"); + break; + + case ModelSpalding14RoseGold: + watchInfo.setProperty("model", "pebble_time_round_rose_gold_14mm"); + break; + + default: + watchInfo.setProperty("model", "pebble_black"); + break; + } + + watchInfo.setProperty("language", m_mgr->m_pebble->language()); + + QJSValue firmware = m_mgr->m_engine->newObject(); + QString version = m_mgr->m_pebble->softwareVersion().remove("v"); + QStringList versionParts = version.split("."); + + if (versionParts.count() >= 1) { + firmware.setProperty("major", versionParts[0].toInt()); + } + + if (versionParts.count() >= 2) { + firmware.setProperty("minor", versionParts[1].toInt()); + } + + if (versionParts.count() >= 3) { + if (versionParts[2].contains("-")) { + QStringList patchParts = version.split("-"); + firmware.setProperty("patch", patchParts[0].toInt()); + firmware.setProperty("suffix", patchParts[1]); + } else { + firmware.setProperty("patch", versionParts[2].toInt()); + firmware.setProperty("suffix", ""); + } + } + + watchInfo.setProperty("firmware", firmware); + return watchInfo; +} + +void JSKitPebble::openURL(const QUrl &url) +{ + emit m_mgr->openURL(m_appInfo.uuid().toString(), url.toString()); +} + +QJSValue JSKitPebble::createXMLHttpRequest() +{ + JSKitXMLHttpRequest *xhr = new JSKitXMLHttpRequest(m_mgr->engine()); + // Should be deleted by JS engine. + return m_mgr->engine()->newQObject(xhr); +} + +QJSValue JSKitPebble::buildAckEventObject(uint transaction, const QString &message) const +{ + QJSEngine *engine = m_mgr->engine(); + QJSValue eventObj = engine->newObject(); + QJSValue dataObj = engine->newObject(); + + dataObj.setProperty("transactionId", engine->toScriptValue(transaction)); + eventObj.setProperty("data", dataObj); + + if (!message.isEmpty()) { + QJSValue errorObj = engine->newObject(); + + errorObj.setProperty("message", engine->toScriptValue(message)); + eventObj.setProperty("error", errorObj); + } + + return eventObj; +} + +void JSKitPebble::invokeCallbacks(const QString &type, const QJSValueList &args) +{ + if (!m_listeners.contains(type)) return; + QList<QJSValue> &callbacks = m_listeners[type]; + + for (QList<QJSValue>::iterator it = callbacks.begin(); it != callbacks.end(); ++it) { + qCDebug(l) << "invoking callback" << type << it->toString(); + QJSValue result = it->call(args); + if (result.isError()) { + qCWarning(l) << "error while invoking callback" + << type << it->toString() << ":" + << JSKitManager::describeError(result); + } + } +} diff --git a/rockworkd/libpebble/jskit/jskitpebble.h b/rockworkd/libpebble/jskit/jskitpebble.h new file mode 100644 index 0000000..d9cd670 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitpebble.h @@ -0,0 +1,47 @@ +#ifndef JSKITPEBBLE_P_H +#define JSKITPEBBLE_P_H + +#include <QLoggingCategory> + +#include "jskitmanager.h" +#include "../appinfo.h" + +class JSKitPebble : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitPebble(const AppInfo &appInfo, JSKitManager *mgr, QObject *parent=0); + + Q_INVOKABLE void addEventListener(const QString &type, QJSValue function); + Q_INVOKABLE void removeEventListener(const QString &type, QJSValue function); + + Q_INVOKABLE void showSimpleNotificationOnPebble(const QString &title, const QString &body); + Q_INVOKABLE uint sendAppMessage(QJSValue message, QJSValue callbackForAck = QJSValue(), QJSValue callbackForNack = QJSValue()); + + Q_INVOKABLE void getTimelineToken(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + Q_INVOKABLE void timelineSubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + Q_INVOKABLE void timelineUnsubscribe(const QString &topic, QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + Q_INVOKABLE void timelineSubscriptions(QJSValue successCallback = QJSValue(), QJSValue failureCallback = QJSValue()); + + Q_INVOKABLE QString getAccountToken() const; + Q_INVOKABLE QString getWatchToken() const; + Q_INVOKABLE QJSValue getActiveWatchInfo() const; + + Q_INVOKABLE void openURL(const QUrl &url); + + Q_INVOKABLE QJSValue createXMLHttpRequest(); + + void invokeCallbacks(const QString &type, const QJSValueList &args = QJSValueList()); + +private: + QJSValue buildAckEventObject(uint transaction, const QString &message = QString()) const; + +private: + AppInfo m_appInfo; + JSKitManager *m_mgr; + QHash<QString, QList<QJSValue>> m_listeners; +}; + +#endif // JSKITPEBBLE_P_H diff --git a/rockworkd/libpebble/jskit/jskitperformance.cpp b/rockworkd/libpebble/jskit/jskitperformance.cpp new file mode 100644 index 0000000..23b0e08 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitperformance.cpp @@ -0,0 +1,13 @@ +#include "jskitperformance.h" + +JSKitPerformance::JSKitPerformance(QObject *parent) : + QObject(parent), + m_start(QTime::currentTime()) +{ +} + +int JSKitPerformance::now() +{ + QTime now = QTime::currentTime(); + return m_start.msecsTo(now); +} diff --git a/rockworkd/libpebble/jskit/jskitperformance.h b/rockworkd/libpebble/jskit/jskitperformance.h new file mode 100644 index 0000000..5f118be --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitperformance.h @@ -0,0 +1,20 @@ +#ifndef JSKITPERFORMANCE_H +#define JSKITPERFORMANCE_H + +#include <QObject> +#include <QTime> + +class JSKitPerformance : public QObject +{ + Q_OBJECT + +public: + explicit JSKitPerformance(QObject *parent=0); + + Q_INVOKABLE int now(); + +private: + QTime m_start; +}; + +#endif // JSKITPERFORMANCE_H diff --git a/rockworkd/libpebble/jskit/jskitsetup.js b/rockworkd/libpebble/jskit/jskitsetup.js new file mode 100644 index 0000000..340c4f1 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitsetup.js @@ -0,0 +1,196 @@ +//Borrowed from https://github.com/pebble/pypkjs/blob/master/pypkjs/javascript/runtime.py#L17 +_jskit.make_proxies = function(proxy, origin, names) { + names.forEach(function(name) { + proxy[name] = eval("(function " + name + "() { return origin[name].apply(origin, arguments); })"); + }); + + return proxy; +} + +_jskit.make_properties = function(proxy, origin, names) { + names.forEach(function(name) { + Object.defineProperty(proxy, name, { + configurable: false, + enumerable: true, + get: function() { + return origin[name]; + }, + set: function(value) { + origin[name] = value; + } + }); + }); + + return proxy; +} + +Pebble = new (function() { + _jskit.make_proxies(this, _jskit.pebble, + ['sendAppMessage', 'showSimpleNotificationOnPebble', 'getAccountToken', 'getWatchToken', + 'addEventListener', 'removeEventListener', 'openURL', 'getTimelineToken', 'timelineSubscribe', + 'timelineUnsubscribe', 'timelineSubscriptions', 'getActiveWatchInfo'] + ); +})(); + +performance = new (function() { + _jskit.make_proxies(this, _jskit.performance, ['now']); +})(); + +function XMLHttpRequest() { + var xhr = _jskit.pebble.createXMLHttpRequest(); + _jskit.make_proxies(this, xhr, + ['open', 'setRequestHeader', 'overrideMimeType', 'send', 'getResponseHeader', + 'getAllResponseHeaders', 'abort', 'addEventListener', 'removeEventListener']); + _jskit.make_properties(this, xhr, + ['readyState', 'response', 'responseText', 'responseType', 'status', + 'statusText', 'timeout', 'onreadystatechange', 'ontimeout', 'onload', + 'onloadstart', 'onloadend', 'onprogress', 'onerror', 'onabort']); + + this.UNSENT = 0; + this.OPENED = 1; + this.HEADERS_RECEIVED = 2; + this.LOADING = 3; + this.DONE = 4; +} + +function setInterval(func, time) { + return _jskit.timer.setInterval(func, time); +} + +function clearInterval(id) { + _jskit.timer.clearInterval(id); +} + +function setTimeout(func, time) { + return _jskit.timer.setTimeout(func, time); +} + +function clearTimeout(id) { + _jskit.timer.clearTimeout(id); +} + +navigator.geolocation = new (function() { + _jskit.make_proxies(this, _jskit.geolocation, + ['getCurrentPosition', 'watchPosition', 'clearWatch'] + ); +})(); + +console = new (function() { + _jskit.make_proxies(this, _jskit.console, + ['log', 'warn', 'error', 'info'] + ); +})(); + +/*localStorage = new (function() { + _jskit.make_proxies(this, _jskit.localstorage, + ['clear', 'getItem', 'setItem', 'removeItem', 'key'] + ); + + _jskit.make_properties(this, _jskit.localstorage, + ['length'] + ); +})();*/ + +//It appears that Proxy is not available since Qt is using Javascript v5 +/*(function() { + var proxy = _jskit.make_proxies({}, _jskit.localstorage, ['set', 'has', 'deleteProperty', 'keys', 'enumerate']); + var methods = _jskit.make_proxies({}, _jskit.localstorage, ['clear', 'getItem', 'setItem', 'removeItem', 'key']); + proxy.get = function get(p, name) { return methods[name] || _jskit.localstorage.get(p, name); } + this.localStorage = Proxy.create(proxy); +})();*/ + +//inspired by https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage +Object.defineProperty(window, "localStorage", new (function () { + var storage = {}; + Object.defineProperty(storage, "getItem", { + value: function (key) { + var value = null; + if (key !== undefined && key !== null && storage[key] !== undefined) { + value = storage[key]; + } + + return value; + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "key", { + value: function (index) { + return Object.keys(storage)[index]; + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "setItem", { + value: function (key, value) { + if (key !== undefined && key !== null) { + _jskit.localstorage.setItem(key, value); + storage[key] = (value && value.toString) ? value.toString() : value; + return true; + } + else { + return false; + } + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "length", { + get: function () { + return Object.keys(storage).length; + }, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "removeItem", { + value: function (key) { + if (key && storage[key]) { + _jskit.localstorage.removeItem(key); + delete storage[key]; + + return true; + } + else { + return false; + } + }, + writable: false, + configurable: false, + enumerable: false + }); + + Object.defineProperty(storage, "clear", { + value: function (key) { + for (var key in storage) { + storage.removeItem(key); + } + + return true; + }, + writable: false, + configurable: false, + enumerable: false + }); + + this.get = function () { + return storage; + }; + + this.configurable = false; + this.enumerable = true; +})()); + +(function() { + var keys = _jskit.localstorage.keys(); + for (var index in keys) { + var value = _jskit.localstorage.getItem(keys[index]); + localStorage.setItem(keys[index], value); + } +})(); diff --git a/rockworkd/libpebble/jskit/jskittimer.cpp b/rockworkd/libpebble/jskit/jskittimer.cpp new file mode 100644 index 0000000..6ab5b4a --- /dev/null +++ b/rockworkd/libpebble/jskit/jskittimer.cpp @@ -0,0 +1,77 @@ +#include <QTimerEvent> + +#include "jskittimer.h" + +JSKitTimer::JSKitTimer(QJSEngine *engine) : + QObject(engine), + l(metaObject()->className()), + m_engine(engine) +{ +} + +int JSKitTimer::setInterval(QJSValue expression, int delay) //TODO support optional parameters +{ + qCDebug(l) << "Setting interval for " << delay << "ms: " << expression.toString(); + + if (expression.isString() || expression.isCallable()) { + int timerId = startTimer(delay); + m_intervals.insert(timerId, expression); + + return timerId; + } + + return -1; +} + +void JSKitTimer::clearInterval(int timerId) +{ + qCDebug(l) << "Killing interval " << timerId ; + killTimer(timerId); + m_intervals.remove(timerId); +} + +int JSKitTimer::setTimeout(QJSValue expression, int delay) //TODO support optional parameters +{ + qCDebug(l) << "Setting timeout for " << delay << "ms: " << expression.toString(); + + if (expression.isString() || expression.isCallable()) { + int timerId = startTimer(delay); + m_timeouts.insert(timerId, expression); + + return timerId; + } + + return -1; +} + +void JSKitTimer::clearTimeout(int timerId) +{ + qCDebug(l) << "Killing timeout " << timerId ; + killTimer(timerId); + m_timeouts.remove(timerId); +} + +void JSKitTimer::timerEvent(QTimerEvent *event) +{ + int id = event->timerId(); + + QJSValue expression; // find in either intervals or timeouts + if (m_intervals.contains(id)) { + expression = m_intervals.value(id); + } else if (m_timeouts.contains(id)) { + expression = m_timeouts.value(id); + killTimer(id); // timeouts don't repeat + } else { + qCWarning(l) << "Unknown timer event"; + killTimer(id); // interval nor timeout exist. kill the timer + + return; + } + + if (expression.isCallable()) { // call it if it's a function + expression.call().toString(); + } + else { // otherwise evaluate it + m_engine->evaluate(expression.toString()); + } +} diff --git a/rockworkd/libpebble/jskit/jskittimer.h b/rockworkd/libpebble/jskit/jskittimer.h new file mode 100644 index 0000000..50b394d --- /dev/null +++ b/rockworkd/libpebble/jskit/jskittimer.h @@ -0,0 +1,31 @@ +#ifndef JSKITTIMER_P_H +#define JSKITTIMER_P_H + +#include <QLoggingCategory> +#include <QJSValue> +#include <QJSEngine> + +class JSKitTimer : public QObject +{ + Q_OBJECT + QLoggingCategory l; + +public: + explicit JSKitTimer(QJSEngine *engine); + + Q_INVOKABLE int setInterval(QJSValue expression, int delay); + Q_INVOKABLE void clearInterval(int timerId); + + Q_INVOKABLE int setTimeout(QJSValue expression, int delay); + Q_INVOKABLE void clearTimeout(int timerId); + +protected: + void timerEvent(QTimerEvent *event); + +private: + QJSEngine *m_engine; + QHash<int, QJSValue> m_intervals; + QHash<int, QJSValue> m_timeouts; +}; + +#endif // JSKITTIMER_P_H diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp new file mode 100644 index 0000000..5948683 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.cpp @@ -0,0 +1,318 @@ +#include <QBuffer> +#include <QAuthenticator> +#include <QEventLoop> + +#include "jskitxmlhttprequest.h" +#include "jskitmanager.h" + +JSKitXMLHttpRequest::JSKitXMLHttpRequest(QJSEngine *engine) : + QObject(engine), + l(metaObject()->className()), + m_engine(engine), + m_net(new QNetworkAccessManager(this)), + m_timeout(0), + m_reply(0) +{ + connect(m_net, &QNetworkAccessManager::authenticationRequired, + this, &JSKitXMLHttpRequest::handleAuthenticationRequired); +} + +void JSKitXMLHttpRequest::open(const QString &method, const QString &url, bool async, const QString &username, const QString &password) +{ + if (m_reply) { + m_reply->deleteLater(); + m_reply = 0; + } + + m_username = username; + m_password = password; + m_request = QNetworkRequest(QUrl(url)); + m_verb = method; + m_async = async; + + qCDebug(l) << "opened to URL" << m_request.url().toString() << "Async:" << async; +} + +void JSKitXMLHttpRequest::setRequestHeader(const QString &header, const QString &value) +{ + qCDebug(l) << "setRequestHeader" << header << value; + m_request.setRawHeader(header.toLatin1(), value.toLatin1()); +} + +void JSKitXMLHttpRequest::send(const QJSValue &data) +{ + QByteArray byteData; + + if (data.isUndefined() || data.isNull()) { + // Do nothing, byteData is empty. + } else if (data.isString()) { + byteData = data.toString().toUtf8(); + } else if (data.isObject()) { + if (data.hasProperty("byteLength")) { + // Looks like an ArrayView or an ArrayBufferView! + QJSValue buffer = data.property("buffer"); + if (buffer.isUndefined()) { + // We must assume we've been passed an ArrayBuffer directly + buffer = data; + } + + QJSValue array = data.property("_bytes"); + int byteLength = data.property("byteLength").toInt(); + + if (array.isArray()) { + byteData.reserve(byteLength); + + for (int i = 0; i < byteLength; i++) { + byteData.append(array.property(i).toInt()); + } + + qCDebug(l) << "passed an ArrayBufferView of" << byteData.length() << "bytes"; + } else { + qCWarning(l) << "passed an unknown/invalid ArrayBuffer" << data.toString(); + } + } else { + qCWarning(l) << "passed an unknown object" << data.toString(); + } + + } + + QBuffer *buffer; + if (!byteData.isEmpty()) { + buffer = new QBuffer; + buffer->setData(byteData); + } else { + buffer = 0; + } + + qCDebug(l) << "sending" << m_verb << "to" << m_request.url() << "with" << QString::fromUtf8(byteData); + m_reply = m_net->sendCustomRequest(m_request, m_verb.toLatin1(), buffer); + + connect(m_reply, &QNetworkReply::finished, + this, &JSKitXMLHttpRequest::handleReplyFinished); + connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), + this, &JSKitXMLHttpRequest::handleReplyError); + + if (buffer) { + // So that it gets deleted alongside the reply object. + buffer->setParent(m_reply); + } + + if (!m_async) { + QEventLoop loop; //Hacky way to get QNetworkReply be synchronous + + connect(m_reply, &QNetworkReply::finished, + &loop, &QEventLoop::quit); + connect(m_reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), + &loop, &QEventLoop::quit); + + loop.exec(); + } +} + +void JSKitXMLHttpRequest::abort() +{ + if (m_reply) { + m_reply->deleteLater(); + m_reply = 0; + } +} + +QJSValue JSKitXMLHttpRequest::onload() const +{ + return m_onload; +} + +void JSKitXMLHttpRequest::setOnload(const QJSValue &value) +{ + m_onload = value; +} + +QJSValue JSKitXMLHttpRequest::onreadystatechange() const +{ + return m_onreadystatechange; +} + +void JSKitXMLHttpRequest::setOnreadystatechange(const QJSValue &value) +{ + m_onreadystatechange = value; +} + +QJSValue JSKitXMLHttpRequest::ontimeout() const +{ + return m_ontimeout; +} + +void JSKitXMLHttpRequest::setOntimeout(const QJSValue &value) +{ + m_ontimeout = value; +} + +QJSValue JSKitXMLHttpRequest::onerror() const +{ + return m_onerror; +} + +void JSKitXMLHttpRequest::setOnerror(const QJSValue &value) +{ + m_onerror = value; +} + +uint JSKitXMLHttpRequest::readyState() const +{ + if (!m_reply) { + return UNSENT; + } else if (m_reply->isFinished()) { + return DONE; + } else { + return LOADING; + } +} + +uint JSKitXMLHttpRequest::timeout() const +{ + return m_timeout; +} + +void JSKitXMLHttpRequest::setTimeout(uint value) +{ + m_timeout = value; + // TODO Handle fetch in-progress. +} + +uint JSKitXMLHttpRequest::status() const +{ + if (!m_reply || !m_reply->isFinished()) { + return 0; + } else { + return m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt(); + } +} + +QString JSKitXMLHttpRequest::statusText() const +{ + if (!m_reply || !m_reply->isFinished()) { + return QString(); + } else { + return m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + } +} + +QString JSKitXMLHttpRequest::responseType() const +{ + return m_responseType; +} + +void JSKitXMLHttpRequest::setResponseType(const QString &type) +{ + qCDebug(l) << "response type set to" << type; + m_responseType = type; +} + +QJSValue JSKitXMLHttpRequest::response() const +{ + if (m_responseType.isEmpty() || m_responseType == "text") { + return m_engine->toScriptValue(QString::fromUtf8(m_response)); + } else if (m_responseType == "arraybuffer") { + QJSValue arrayBufferProto = m_engine->globalObject().property("ArrayBuffer").property("prototype"); + QJSValue arrayBuf = m_engine->newObject(); + + if (!arrayBufferProto.isUndefined()) { + arrayBuf.setPrototype(arrayBufferProto); + arrayBuf.setProperty("byteLength", m_engine->toScriptValue<uint>(m_response.size())); + + QJSValue array = m_engine->newArray(m_response.size()); + for (int i = 0; i < m_response.size(); i++) { + array.setProperty(i, m_engine->toScriptValue<int>(m_response[i])); + } + + arrayBuf.setProperty("_bytes", array); + qCDebug(l) << "returning ArrayBuffer of" << m_response.size() << "bytes"; + } else { + qCWarning(l) << "Cannot find proto of ArrayBuffer"; + } + + return arrayBuf; + } else { + qCWarning(l) << "unsupported responseType:" << m_responseType; + return m_engine->toScriptValue<void*>(0); + } +} + +QString JSKitXMLHttpRequest::responseText() const +{ + return QString::fromUtf8(m_response); +} + +void JSKitXMLHttpRequest::handleReplyFinished() +{ + if (!m_reply) { + qCDebug(l) << "reply finished too late"; + return; + } + + m_response = m_reply->readAll(); + qCDebug(l) << "reply finished, reply text:" << QString::fromUtf8(m_response) << "status:" << status(); + + emit readyStateChanged(); + emit statusChanged(); + emit statusTextChanged(); + emit responseChanged(); + emit responseTextChanged(); + + if (m_onload.isCallable()) { + qCDebug(l) << "going to call onload handler:" << m_onload.toString(); + + QJSValue result = m_onload.callWithInstance(m_engine->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onload handler:" << JSKitManager::describeError(result); + } + } else { + qCDebug(l) << "No onload set"; + } + + if (m_onreadystatechange.isCallable()) { + qCDebug(l) << "going to call onreadystatechange handler:" << m_onreadystatechange.toString(); + QJSValue result = m_onreadystatechange.callWithInstance(m_engine->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onreadystatechange handler:" << JSKitManager::describeError(result); + } + } +} + +void JSKitXMLHttpRequest::handleReplyError(QNetworkReply::NetworkError code) +{ + if (!m_reply) { + qCDebug(l) << "reply error too late"; + return; + } + + qCDebug(l) << "reply error" << code; + + emit readyStateChanged(); + emit statusChanged(); + emit statusTextChanged(); + + if (m_onerror.isCallable()) { + qCDebug(l) << "going to call onerror handler:" << m_onload.toString(); + QJSValue result = m_onerror.callWithInstance(m_engine->newQObject(this)); + if (result.isError()) { + qCWarning(l) << "JS error on onerror handler:" << JSKitManager::describeError(result); + } + } +} + +void JSKitXMLHttpRequest::handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth) +{ + if (m_reply == reply) { + qCDebug(l) << "authentication required"; + + if (!m_username.isEmpty() || !m_password.isEmpty()) { + qCDebug(l) << "using provided authorization:" << m_username; + + auth->setUser(m_username); + auth->setPassword(m_password); + } else { + qCDebug(l) << "no username or password provided"; + } + } +} diff --git a/rockworkd/libpebble/jskit/jskitxmlhttprequest.h b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h new file mode 100644 index 0000000..70b8136 --- /dev/null +++ b/rockworkd/libpebble/jskit/jskitxmlhttprequest.h @@ -0,0 +1,96 @@ +#ifndef JSKITXMLHTTPREQUEST_P_H +#define JSKITXMLHTTPREQUEST_P_H + +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QJSEngine> +#include <QLoggingCategory> + +class JSKitXMLHttpRequest : public QObject +{ + Q_OBJECT + QLoggingCategory l; + + Q_PROPERTY(QJSValue onload READ onload WRITE setOnload) + Q_PROPERTY(QJSValue onreadystatechange READ onreadystatechange WRITE setOnreadystatechange) + Q_PROPERTY(QJSValue ontimeout READ ontimeout WRITE setOntimeout) + Q_PROPERTY(QJSValue onerror READ onerror WRITE setOnerror) + Q_PROPERTY(uint readyState READ readyState NOTIFY readyStateChanged) + Q_PROPERTY(uint timeout READ timeout WRITE setTimeout) + Q_PROPERTY(uint status READ status NOTIFY statusChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(QString responseType READ responseType WRITE setResponseType) + Q_PROPERTY(QJSValue response READ response NOTIFY responseChanged) + Q_PROPERTY(QString responseText READ responseText NOTIFY responseTextChanged) + +public: + explicit JSKitXMLHttpRequest(QJSEngine *engine); + + enum ReadyStates { + UNSENT = 0, + OPENED = 1, + HEADERS_RECEIVED = 2, + LOADING = 3, + DONE = 4 + }; + Q_ENUMS(ReadyStates) + + Q_INVOKABLE void open(const QString &method, const QString &url, bool async = true, const QString &username = QString(), const QString &password = QString()); + Q_INVOKABLE void setRequestHeader(const QString &header, const QString &value); + Q_INVOKABLE void send(const QJSValue &data = QJSValue(QJSValue::NullValue)); + Q_INVOKABLE void abort(); + + QJSValue onload() const; + void setOnload(const QJSValue &value); + QJSValue onreadystatechange() const; + void setOnreadystatechange(const QJSValue &value); + QJSValue ontimeout() const; + void setOntimeout(const QJSValue &value); + QJSValue onerror() const; + void setOnerror(const QJSValue &value); + + uint readyState() const; + + uint timeout() const; + void setTimeout(uint value); + + uint status() const; + QString statusText() const; + + QString responseType() const; + void setResponseType(const QString& type); + + QJSValue response() const; + QString responseText() const; + +signals: + void readyStateChanged(); + void statusChanged(); + void statusTextChanged(); + void responseChanged(); + void responseTextChanged(); + +private slots: + void handleReplyFinished(); + void handleReplyError(QNetworkReply::NetworkError code); + void handleAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth); + +private: + QJSEngine *m_engine; + QNetworkAccessManager *m_net; + QString m_verb; + bool m_async = true; + uint m_timeout; + QString m_username; + QString m_password; + QNetworkRequest m_request; + QNetworkReply *m_reply; + QString m_responseType; + QByteArray m_response; + QJSValue m_onload; + QJSValue m_onreadystatechange; + QJSValue m_ontimeout; + QJSValue m_onerror; +}; + +#endif // JSKITXMLHTTPREQUEST_P_H diff --git a/rockworkd/libpebble/jskit/typedarray.js b/rockworkd/libpebble/jskit/typedarray.js new file mode 100644 index 0000000..d4e00c6 --- /dev/null +++ b/rockworkd/libpebble/jskit/typedarray.js @@ -0,0 +1,1037 @@ +/* + Copyright (c) 2010, Linden Research, Inc. + Copyright (c) 2014, Joshua Bell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + $/LicenseInfo$ + */ + +// Original can be found at: +// https://bitbucket.org/lindenlab/llsd +// Modifications by Joshua Bell inexorabletash@gmail.com +// https://github.com/inexorabletash/polyfill + +// ES3/ES5 implementation of the Krhonos Typed Array Specification +// Ref: http://www.khronos.org/registry/typedarray/specs/latest/ +// Date: 2011-02-01 +// +// Variations: +// * Allows typed_array.get/set() as alias for subscripts (typed_array[]) +// * Gradually migrating structure from Khronos spec to ES6 spec +(function(global) { + 'use strict'; + var undefined = (void 0); // Paranoia + + // Beyond this value, index getters/setters (i.e. array[0], array[1]) are so slow to + // create, and consume so much memory, that the browser appears frozen. + var MAX_ARRAY_LENGTH = 1e5; + + // Approximations of internal ECMAScript conversion functions + function Type(v) { + switch(typeof v) { + case 'undefined': return 'undefined'; + case 'boolean': return 'boolean'; + case 'number': return 'number'; + case 'string': return 'string'; + default: return v === null ? 'null' : 'object'; + } + } + + // Class returns internal [[Class]] property, used to avoid cross-frame instanceof issues: + function Class(v) { return Object.prototype.toString.call(v).replace(/^\[object *|\]$/g, ''); } + function IsCallable(o) { return typeof o === 'function'; } + function ToObject(v) { + if (v === null || v === undefined) throw TypeError(); + return Object(v); + } + function ToInt32(v) { return v >> 0; } + function ToUint32(v) { return v >> 0; } //ROCKWORK HACK ALERT: it appears that QT doesn't do the >>> properly, using >> here instead (should be close enough) + + // Snapshot intrinsics + var LN2 = Math.LN2, + abs = Math.abs, + floor = Math.floor, + log = Math.log, + max = Math.max, + min = Math.min, + pow = Math.pow, + round = Math.round; + + // emulate ES5 getter/setter API using legacy APIs + // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx + // (second clause tests for Object.defineProperty() in IE<9 that only supports extending DOM prototypes, but + // note that IE<9 does not support __defineGetter__ or __defineSetter__ so it just renders the method harmless) + + (function() { + var orig = Object.defineProperty; + var dom_only = !(function(){try{return Object.defineProperty({},'x',{});}catch(_){return false;}}()); + + if (!orig || dom_only) { + Object.defineProperty = function (o, prop, desc) { + // In IE8 try built-in implementation for defining properties on DOM prototypes. + if (orig) + try { return orig(o, prop, desc); } catch (_) {} + if (o !== Object(o)) + throw TypeError('Object.defineProperty called on non-object'); + if (Object.prototype.__defineGetter__ && ('get' in desc)) + Object.prototype.__defineGetter__.call(o, prop, desc.get); + if (Object.prototype.__defineSetter__ && ('set' in desc)) + Object.prototype.__defineSetter__.call(o, prop, desc.set); + if ('value' in desc) + o[prop] = desc.value; + return o; + }; + } + }()); + + // ES5: Make obj[index] an alias for obj._getter(index)/obj._setter(index, value) + // for index in 0 ... obj.length + function makeArrayAccessors(obj) { + if (obj.length > MAX_ARRAY_LENGTH) throw RangeError('Array too large for polyfill'); + + function makeArrayAccessor(index) { + Object.defineProperty(obj, index, { + 'get': function() { return obj._getter(index); }, + 'set': function(v) { obj._setter(index, v); }, + enumerable: true, + configurable: false + }); + } + + var i; + for (i = 0; i < obj.length; i += 1) { + makeArrayAccessor(i); + } + } + + // Internal conversion functions: + // pack<Type>() - take a number (interpreted as Type), output a byte array + // unpack<Type>() - take a byte array, output a Type-like number + + function as_signed(value, bits) { var s = 32 - bits; return (value << s) >> s; } + function as_unsigned(value, bits) { var s = 32 - bits; return (value << s) >>> s; } + + function packI8(n) { return [n & 0xff]; } + function unpackI8(bytes) { return as_signed(bytes[0], 8); } + + function packU8(n) { return [n & 0xff]; } + function unpackU8(bytes) { return as_unsigned(bytes[0], 8); } + + function packU8Clamped(n) { n = round(Number(n)); return [n < 0 ? 0 : n > 0xff ? 0xff : n & 0xff]; } + + function packI16(n) { return [n & 0xff, (n >> 8) & 0xff]; } + function unpackI16(bytes) { return as_signed(bytes[1] << 8 | bytes[0], 16); } + + function packU16(n) { return [n & 0xff, (n >> 8) & 0xff]; } + function unpackU16(bytes) { return as_unsigned(bytes[1] << 8 | bytes[0], 16); } + + function packI32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; } + function unpackI32(bytes) { return as_signed(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); } + + function packU32(n) { return [n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]; } + function unpackU32(bytes) { return as_unsigned(bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0], 32); } + + function packIEEE754(v, ebits, fbits) { + + var bias = (1 << (ebits - 1)) - 1; + + function roundToEven(n) { + var w = floor(n), f = n - w; + if (f < 0.5) + return w; + if (f > 0.5) + return w + 1; + return w % 2 ? w + 1 : w; + } + + // Compute sign, exponent, fraction + var s, e, f; + if (v !== v) { + // NaN + // http://dev.w3.org/2006/webapi/WebIDL/#es-type-mapping + e = (1 << ebits) - 1; f = pow(2, fbits - 1); s = 0; + } else if (v === Infinity || v === -Infinity) { + e = (1 << ebits) - 1; f = 0; s = (v < 0) ? 1 : 0; + } else if (v === 0) { + e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0; + } else { + s = v < 0; + v = abs(v); + + if (v >= pow(2, 1 - bias)) { + // Normalized + e = min(floor(log(v) / LN2), 1023); + var significand = v / pow(2, e); + if (significand < 1) { + e -= 1; + significand *= 2; + } + if (significand >= 2) { + e += 1; + significand /= 2; + } + var d = pow(2, fbits); + f = roundToEven(significand * d) - d; + e += bias; + if (f / d >= 1) { + e += 1; + f = 0; + } + if (e > 2 * bias) { + // Overflow + e = (1 << ebits) - 1; + f = 0; + } + } else { + // Denormalized + e = 0; + f = roundToEven(v / pow(2, 1 - bias - fbits)); + } + } + + // Pack sign, exponent, fraction + var bits = [], i; + for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = floor(f / 2); } + for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = floor(e / 2); } + bits.push(s ? 1 : 0); + bits.reverse(); + var str = bits.join(''); + + // Bits to bytes + var bytes = []; + while (str.length) { + bytes.unshift(parseInt(str.substring(0, 8), 2)); + str = str.substring(8); + } + return bytes; + } + + function unpackIEEE754(bytes, ebits, fbits) { + // Bytes to bits + var bits = [], i, j, b, str, + bias, s, e, f; + + for (i = 0; i < bytes.length; ++i) { + b = bytes[i]; + for (j = 8; j; j -= 1) { + bits.push(b % 2 ? 1 : 0); b = b >> 1; + } + } + bits.reverse(); + str = bits.join(''); + + // Unpack sign, exponent, fraction + bias = (1 << (ebits - 1)) - 1; + s = parseInt(str.substring(0, 1), 2) ? -1 : 1; + e = parseInt(str.substring(1, 1 + ebits), 2); + f = parseInt(str.substring(1 + ebits), 2); + + // Produce number + if (e === (1 << ebits) - 1) { + return f !== 0 ? NaN : s * Infinity; + } else if (e > 0) { + // Normalized + return s * pow(2, e - bias) * (1 + f / pow(2, fbits)); + } else if (f !== 0) { + // Denormalized + return s * pow(2, -(bias - 1)) * (f / pow(2, fbits)); + } else { + return s < 0 ? -0 : 0; + } + } + + function unpackF64(b) { return unpackIEEE754(b, 11, 52); } + function packF64(v) { return packIEEE754(v, 11, 52); } + function unpackF32(b) { return unpackIEEE754(b, 8, 23); } + function packF32(v) { return packIEEE754(v, 8, 23); } + + // + // 3 The ArrayBuffer Type + // + + (function() { + + function ArrayBuffer(length) { + length = ToInt32(length); + if (length < 0) throw RangeError('ArrayBuffer size is not a small enough positive integer.'); + Object.defineProperty(this, 'byteLength', {value: length}); + Object.defineProperty(this, '_bytes', {value: Array(length)}); + + for (var i = 0; i < length; i += 1) + this._bytes[i] = 0; + } + + global.ArrayBuffer = global.ArrayBuffer || ArrayBuffer; + + // + // 5 The Typed Array View Types + // + + function $TypedArray$() { + + // %TypedArray% ( length ) + if (!arguments.length || typeof arguments[0] !== 'object') { + return (function(length) { + length = ToInt32(length); + if (length < 0) throw RangeError('length is not a small enough positive integer.'); + Object.defineProperty(this, 'length', {value: length}); + Object.defineProperty(this, 'byteLength', {value: length * this.BYTES_PER_ELEMENT}); + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(this.byteLength)}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + + }).apply(this, arguments); + } + + // %TypedArray% ( typedArray ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + arguments[0] instanceof $TypedArray$) { + return (function(typedArray){ + if (this.constructor !== typedArray.constructor) throw TypeError(); + + var byteLength = typedArray.length * this.BYTES_PER_ELEMENT; + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + Object.defineProperty(this, 'length', {value: typedArray.length}); + + for (var i = 0; i < this.length; i += 1) + this._setter(i, typedArray._getter(i)); + + }).apply(this, arguments); + } + + // %TypedArray% ( array ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + !(arguments[0] instanceof $TypedArray$) && + !(arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { + return (function(array) { + + var byteLength = array.length * this.BYTES_PER_ELEMENT; + Object.defineProperty(this, 'buffer', {value: new ArrayBuffer(byteLength)}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: 0}); + Object.defineProperty(this, 'length', {value: array.length}); + + for (var i = 0; i < this.length; i += 1) { + var s = array[i]; + this._setter(i, Number(s)); + } + }).apply(this, arguments); + } + + // %TypedArray% ( buffer, byteOffset=0, length=undefined ) + if (arguments.length >= 1 && + Type(arguments[0]) === 'object' && + (arguments[0] instanceof ArrayBuffer || Class(arguments[0]) === 'ArrayBuffer')) { + return (function(buffer, byteOffset, length) { + + byteOffset = ToUint32(byteOffset); + if (byteOffset > buffer.byteLength) + throw RangeError('byteOffset out of range'); + + // The given byteOffset must be a multiple of the element + // size of the specific type, otherwise an exception is raised. + if (byteOffset % this.BYTES_PER_ELEMENT) + throw RangeError('buffer length minus the byteOffset is not a multiple of the element size.'); + + if (length === undefined) { + var byteLength = buffer.byteLength - byteOffset; + if (byteLength % this.BYTES_PER_ELEMENT) + throw RangeError('length of buffer minus byteOffset not a multiple of the element size'); + length = byteLength / this.BYTES_PER_ELEMENT; + + } else { + length = ToUint32(length); + byteLength = length * this.BYTES_PER_ELEMENT; + } + + if ((byteOffset + byteLength) > buffer.byteLength) + throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); + + Object.defineProperty(this, 'buffer', {value: buffer}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: byteOffset}); + Object.defineProperty(this, 'length', {value: length}); + + }).apply(this, arguments); + } + + // %TypedArray% ( all other argument combinations ) + throw TypeError(); + } + + // Properties of the %TypedArray Instrinsic Object + + // %TypedArray%.from ( source , mapfn=undefined, thisArg=undefined ) + Object.defineProperty($TypedArray$, 'from', {value: function(iterable) { + return new this(iterable); + }}); + + // %TypedArray%.of ( ...items ) + Object.defineProperty($TypedArray$, 'of', {value: function(/*...items*/) { + return new this(arguments); + }}); + + // %TypedArray%.prototype + var $TypedArrayPrototype$ = {}; + $TypedArray$.prototype = $TypedArrayPrototype$; + + // WebIDL: getter type (unsigned long index); + Object.defineProperty($TypedArray$.prototype, '_getter', {value: function(index) { + if (arguments.length < 1) throw SyntaxError('Not enough arguments'); + + index = ToUint32(index); + if (index >= this.length) + return undefined; + + var bytes = [], i, o; + for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; + i < this.BYTES_PER_ELEMENT; + i += 1, o += 1) { + bytes.push(this.buffer._bytes[o]); + } + return this._unpack(bytes); + }}); + + // NONSTANDARD: convenience alias for getter: type get(unsigned long index); + Object.defineProperty($TypedArray$.prototype, 'get', {value: $TypedArray$.prototype._getter}); + + // WebIDL: setter void (unsigned long index, type value); + Object.defineProperty($TypedArray$.prototype, '_setter', {value: function(index, value) { + if (arguments.length < 2) throw SyntaxError('Not enough arguments'); + + index = ToUint32(index); + if (index >= this.length) + return; + + var bytes = this._pack(value), i, o; + for (i = 0, o = this.byteOffset + index * this.BYTES_PER_ELEMENT; + i < this.BYTES_PER_ELEMENT; + i += 1, o += 1) { + this.buffer._bytes[o] = bytes[i]; + } + }}); + + // get %TypedArray%.prototype.buffer + // get %TypedArray%.prototype.byteLength + // get %TypedArray%.prototype.byteOffset + // -- applied directly to the object in the constructor + + // %TypedArray%.prototype.constructor + Object.defineProperty($TypedArray$.prototype, 'constructor', {value: $TypedArray$}); + + // %TypedArray%.prototype.copyWithin (target, start, end = this.length ) + Object.defineProperty($TypedArray$.prototype, 'copyWithin', {value: function(target, start) { + var end = arguments[2]; + + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + len = max(len, 0); + var relativeTarget = ToInt32(target); + var to; + if (relativeTarget < 0) + to = max(len + relativeTarget, 0); + else + to = min(relativeTarget, len); + var relativeStart = ToInt32(start); + var from; + if (relativeStart < 0) + from = max(len + relativeStart, 0); + else + from = min(relativeStart, len); + var relativeEnd; + if (end === undefined) + relativeEnd = len; + else + relativeEnd = ToInt32(end); + var final; + if (relativeEnd < 0) + final = max(len + relativeEnd, 0); + else + final = min(relativeEnd, len); + var count = min(final - from, len - to); + var direction; + if (from < to && to < from + count) { + direction = -1; + from = from + count - 1; + to = to + count - 1; + } else { + direction = 1; + } + while (count > 0) { + o._setter(to, o._getter(from)); + from = from + direction; + to = to + direction; + count = count - 1; + } + return o; + }}); + + // %TypedArray%.prototype.entries ( ) + // -- defined in es6.js to shim browsers w/ native TypedArrays + + // %TypedArray%.prototype.every ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'every', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisArg = arguments[1]; + for (var i = 0; i < len; i++) { + if (!callbackfn.call(thisArg, t._getter(i), i, t)) + return false; + } + return true; + }}); + + // %TypedArray%.prototype.fill (value, start = 0, end = this.length ) + Object.defineProperty($TypedArray$.prototype, 'fill', {value: function(value) { + var start = arguments[1], + end = arguments[2]; + + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + len = max(len, 0); + var relativeStart = ToInt32(start); + var k; + if (relativeStart < 0) + k = max((len + relativeStart), 0); + else + k = min(relativeStart, len); + var relativeEnd; + if (end === undefined) + relativeEnd = len; + else + relativeEnd = ToInt32(end); + var final; + if (relativeEnd < 0) + final = max((len + relativeEnd), 0); + else + final = min(relativeEnd, len); + while (k < final) { + o._setter(k, value); + k += 1; + } + return o; + }}); + + // %TypedArray%.prototype.filter ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'filter', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var res = []; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + var val = t._getter(i); // in case fun mutates this + if (callbackfn.call(thisp, val, i, t)) + res.push(val); + } + return new this.constructor(res); + }}); + + // %TypedArray%.prototype.find (predicate, thisArg = undefined) + Object.defineProperty($TypedArray$.prototype, 'find', {value: function(predicate) { + var o = ToObject(this); + var lenValue = o.length; + var len = ToUint32(lenValue); + if (!IsCallable(predicate)) throw TypeError(); + var t = arguments.length > 1 ? arguments[1] : undefined; + var k = 0; + while (k < len) { + var kValue = o._getter(k); + var testResult = predicate.call(t, kValue, k, o); + if (Boolean(testResult)) + return kValue; + ++k; + } + return undefined; + }}); + + // %TypedArray%.prototype.findIndex ( predicate, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'findIndex', {value: function(predicate) { + var o = ToObject(this); + var lenValue = o.length; + var len = ToUint32(lenValue); + if (!IsCallable(predicate)) throw TypeError(); + var t = arguments.length > 1 ? arguments[1] : undefined; + var k = 0; + while (k < len) { + var kValue = o._getter(k); + var testResult = predicate.call(t, kValue, k, o); + if (Boolean(testResult)) + return k; + ++k; + } + return -1; + }}); + + // %TypedArray%.prototype.forEach ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'forEach', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + callbackfn.call(thisp, t._getter(i), i, t); + }}); + + // %TypedArray%.prototype.indexOf (searchElement, fromIndex = 0 ) + Object.defineProperty($TypedArray$.prototype, 'indexOf', {value: function(searchElement) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (len === 0) return -1; + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * floor(abs(n)); + } + } + if (n >= len) return -1; + var k = n >= 0 ? n : max(len - abs(n), 0); + for (; k < len; k++) { + if (t._getter(k) === searchElement) { + return k; + } + } + return -1; + }}); + + // %TypedArray%.prototype.join ( separator ) + Object.defineProperty($TypedArray$.prototype, 'join', {value: function(separator) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var tmp = Array(len); + for (var i = 0; i < len; ++i) + tmp[i] = t._getter(i); + return tmp.join(separator === undefined ? ',' : separator); // Hack for IE7 + }}); + + // %TypedArray%.prototype.keys ( ) + // -- defined in es6.js to shim browsers w/ native TypedArrays + + // %TypedArray%.prototype.lastIndexOf ( searchElement, fromIndex = this.length-1 ) + Object.defineProperty($TypedArray$.prototype, 'lastIndexOf', {value: function(searchElement) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (len === 0) return -1; + var n = len; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n !== n) { + n = 0; + } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * floor(abs(n)); + } + } + var k = n >= 0 ? min(n, len - 1) : len - abs(n); + for (; k >= 0; k--) { + if (t._getter(k) === searchElement) + return k; + } + return -1; + }}); + + // get %TypedArray%.prototype.length + // -- applied directly to the object in the constructor + + // %TypedArray%.prototype.map ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'map', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var res = []; res.length = len; + var thisp = arguments[1]; + for (var i = 0; i < len; i++) + res[i] = callbackfn.call(thisp, t._getter(i), i, t); + return new this.constructor(res); + }}); + + // %TypedArray%.prototype.reduce ( callbackfn [, initialValue] ) + Object.defineProperty($TypedArray$.prototype, 'reduce', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + // no value to return if no initial value and an empty array + if (len === 0 && arguments.length === 1) throw TypeError(); + var k = 0; + var accumulator; + if (arguments.length >= 2) { + accumulator = arguments[1]; + } else { + accumulator = t._getter(k++); + } + while (k < len) { + accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); + k++; + } + return accumulator; + }}); + + // %TypedArray%.prototype.reduceRight ( callbackfn [, initialValue] ) + Object.defineProperty($TypedArray$.prototype, 'reduceRight', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + // no value to return if no initial value, empty array + if (len === 0 && arguments.length === 1) throw TypeError(); + var k = len - 1; + var accumulator; + if (arguments.length >= 2) { + accumulator = arguments[1]; + } else { + accumulator = t._getter(k--); + } + while (k >= 0) { + accumulator = callbackfn.call(undefined, accumulator, t._getter(k), k, t); + k--; + } + return accumulator; + }}); + + // %TypedArray%.prototype.reverse ( ) + Object.defineProperty($TypedArray$.prototype, 'reverse', {value: function() { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var half = floor(len / 2); + for (var i = 0, j = len - 1; i < half; ++i, --j) { + var tmp = t._getter(i); + t._setter(i, t._getter(j)); + t._setter(j, tmp); + } + return t; + }}); + + // %TypedArray%.prototype.set(array, offset = 0 ) + // %TypedArray%.prototype.set(typedArray, offset = 0 ) + // WebIDL: void set(TypedArray array, optional unsigned long offset); + // WebIDL: void set(sequence<type> array, optional unsigned long offset); + Object.defineProperty($TypedArray$.prototype, 'set', {value: function(index, value) { + if (arguments.length < 1) throw SyntaxError('Not enough arguments'); + var array, sequence, offset, len, + i, s, d, + byteOffset, byteLength, tmp; + + if (typeof arguments[0] === 'object' && arguments[0].constructor === this.constructor) { + // void set(TypedArray array, optional unsigned long offset); + array = arguments[0]; + offset = ToUint32(arguments[1]); + + if (offset + array.length > this.length) { + throw RangeError('Offset plus length of array is out of range'); + } + + byteOffset = this.byteOffset + offset * this.BYTES_PER_ELEMENT; + byteLength = array.length * this.BYTES_PER_ELEMENT; + + if (array.buffer === this.buffer) { + tmp = []; + for (i = 0, s = array.byteOffset; i < byteLength; i += 1, s += 1) { + tmp[i] = array.buffer._bytes[s]; + } + for (i = 0, d = byteOffset; i < byteLength; i += 1, d += 1) { + this.buffer._bytes[d] = tmp[i]; + } + } else { + for (i = 0, s = array.byteOffset, d = byteOffset; + i < byteLength; i += 1, s += 1, d += 1) { + this.buffer._bytes[d] = array.buffer._bytes[s]; + } + } + } else if (typeof arguments[0] === 'object' && typeof arguments[0].length !== 'undefined') { + // void set(sequence<type> array, optional unsigned long offset); + sequence = arguments[0]; + len = ToUint32(sequence.length); + offset = ToUint32(arguments[1]); + + if (offset + len > this.length) { + throw RangeError('Offset plus length of array is out of range'); + } + + for (i = 0; i < len; i += 1) { + s = sequence[i]; + this._setter(offset + i, Number(s)); + } + } else { + throw TypeError('Unexpected argument type(s)'); + } + }}); + + // %TypedArray%.prototype.slice ( start, end ) + Object.defineProperty($TypedArray$.prototype, 'slice', {value: function(start, end) { + var o = ToObject(this); + var lenVal = o.length; + var len = ToUint32(lenVal); + var relativeStart = ToInt32(start); + var k = (relativeStart < 0) ? max(len + relativeStart, 0) : min(relativeStart, len); + var relativeEnd = (end === undefined) ? len : ToInt32(end); + var final = (relativeEnd < 0) ? max(len + relativeEnd, 0) : min(relativeEnd, len); + var count = final - k; + var c = o.constructor; + var a = new c(count); + var n = 0; + while (k < final) { + var kValue = o._getter(k); + a._setter(n, kValue); + ++k; + ++n; + } + return a; + }}); + + // %TypedArray%.prototype.some ( callbackfn, thisArg = undefined ) + Object.defineProperty($TypedArray$.prototype, 'some', {value: function(callbackfn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + if (!IsCallable(callbackfn)) throw TypeError(); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (callbackfn.call(thisp, t._getter(i), i, t)) { + return true; + } + } + return false; + }}); + + // %TypedArray%.prototype.sort ( comparefn ) + Object.defineProperty($TypedArray$.prototype, 'sort', {value: function(comparefn) { + if (this === undefined || this === null) throw TypeError(); + var t = Object(this); + var len = ToUint32(t.length); + var tmp = Array(len); + for (var i = 0; i < len; ++i) + tmp[i] = t._getter(i); + if (comparefn) tmp.sort(comparefn); else tmp.sort(); // Hack for IE8/9 + for (i = 0; i < len; ++i) + t._setter(i, tmp[i]); + return t; + }}); + + // %TypedArray%.prototype.subarray(begin = 0, end = this.length ) + // WebIDL: TypedArray subarray(long begin, optional long end); + Object.defineProperty($TypedArray$.prototype, 'subarray', {value: function(start, end) { + function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } + + start = ToInt32(start); + end = ToInt32(end); + + if (arguments.length < 1) { start = 0; } + if (arguments.length < 2) { end = this.length; } + + if (start < 0) { start = this.length + start; } + if (end < 0) { end = this.length + end; } + + start = clamp(start, 0, this.length); + end = clamp(end, 0, this.length); + + var len = end - start; + if (len < 0) { + len = 0; + } + + return new this.constructor( + this.buffer, this.byteOffset + start * this.BYTES_PER_ELEMENT, len); + }}); + + // %TypedArray%.prototype.toLocaleString ( ) + // %TypedArray%.prototype.toString ( ) + // %TypedArray%.prototype.values ( ) + // %TypedArray%.prototype [ @@iterator ] ( ) + // get %TypedArray%.prototype [ @@toStringTag ] + // -- defined in es6.js to shim browsers w/ native TypedArrays + + function makeTypedArray(elementSize, pack, unpack) { + // Each TypedArray type requires a distinct constructor instance with + // identical logic, which this produces. + var TypedArray = function() { + Object.defineProperty(this, 'constructor', {value: TypedArray}); + $TypedArray$.apply(this, arguments); + makeArrayAccessors(this); + }; + if ('__proto__' in TypedArray) { + TypedArray.__proto__ = $TypedArray$; + } else { + TypedArray.from = $TypedArray$.from; + TypedArray.of = $TypedArray$.of; + } + + TypedArray.BYTES_PER_ELEMENT = elementSize; + + var TypedArrayPrototype = function() {}; + TypedArrayPrototype.prototype = $TypedArrayPrototype$; + + TypedArray.prototype = new TypedArrayPrototype(); + + Object.defineProperty(TypedArray.prototype, 'BYTES_PER_ELEMENT', {value: elementSize}); + Object.defineProperty(TypedArray.prototype, '_pack', {value: pack}); + Object.defineProperty(TypedArray.prototype, '_unpack', {value: unpack}); + + return TypedArray; + } + + var Int8Array = makeTypedArray(1, packI8, unpackI8); + var Uint8Array = makeTypedArray(1, packU8, unpackU8); + var Uint8ClampedArray = makeTypedArray(1, packU8Clamped, unpackU8); + var Int16Array = makeTypedArray(2, packI16, unpackI16); + var Uint16Array = makeTypedArray(2, packU16, unpackU16); + var Int32Array = makeTypedArray(4, packI32, unpackI32); + var Uint32Array = makeTypedArray(4, packU32, unpackU32); + var Float32Array = makeTypedArray(4, packF32, unpackF32); + var Float64Array = makeTypedArray(8, packF64, unpackF64); + + global.Int8Array = global.Int8Array || Int8Array; + global.Uint8Array = global.Uint8Array || Uint8Array; + global.Uint8ClampedArray = global.Uint8ClampedArray || Uint8ClampedArray; + global.Int16Array = global.Int16Array || Int16Array; + global.Uint16Array = global.Uint16Array || Uint16Array; + global.Int32Array = global.Int32Array || Int32Array; + global.Uint32Array = global.Uint32Array || Uint32Array; + global.Float32Array = global.Float32Array || Float32Array; + global.Float64Array = global.Float64Array || Float64Array; + }()); + + // + // 6 The DataView View Type + // + + (function() { + function r(array, index) { + return IsCallable(array.get) ? array.get(index) : array[index]; + } + + var IS_BIG_ENDIAN = (function() { + var u16array = new Uint16Array([0x1234]), + u8array = new Uint8Array(u16array.buffer); + return r(u8array, 0) === 0x12; + }()); + + // DataView(buffer, byteOffset=0, byteLength=undefined) + // WebIDL: Constructor(ArrayBuffer buffer, + // optional unsigned long byteOffset, + // optional unsigned long byteLength) + function DataView(buffer, byteOffset, byteLength) { + if (!(buffer instanceof ArrayBuffer || Class(buffer) === 'ArrayBuffer')) throw TypeError(); + + byteOffset = ToUint32(byteOffset); + if (byteOffset > buffer.byteLength) + throw RangeError('byteOffset out of range'); + + if (byteLength === undefined) + byteLength = buffer.byteLength - byteOffset; + else + byteLength = ToUint32(byteLength); + + if ((byteOffset + byteLength) > buffer.byteLength) + throw RangeError('byteOffset and length reference an area beyond the end of the buffer'); + + Object.defineProperty(this, 'buffer', {value: buffer}); + Object.defineProperty(this, 'byteLength', {value: byteLength}); + Object.defineProperty(this, 'byteOffset', {value: byteOffset}); + }; + + // get DataView.prototype.buffer + // get DataView.prototype.byteLength + // get DataView.prototype.byteOffset + // -- applied directly to instances by the constructor + + function makeGetter(arrayType) { + return function GetViewValue(byteOffset, littleEndian) { + byteOffset = ToUint32(byteOffset); + + if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) + throw RangeError('Array index out of range'); + + byteOffset += this.byteOffset; + + var uint8Array = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT), + bytes = []; + for (var i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) + bytes.push(r(uint8Array, i)); + + if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) + bytes.reverse(); + + return r(new arrayType(new Uint8Array(bytes).buffer), 0); + }; + } + + Object.defineProperty(DataView.prototype, 'getUint8', {value: makeGetter(Uint8Array)}); + Object.defineProperty(DataView.prototype, 'getInt8', {value: makeGetter(Int8Array)}); + Object.defineProperty(DataView.prototype, 'getUint16', {value: makeGetter(Uint16Array)}); + Object.defineProperty(DataView.prototype, 'getInt16', {value: makeGetter(Int16Array)}); + Object.defineProperty(DataView.prototype, 'getUint32', {value: makeGetter(Uint32Array)}); + Object.defineProperty(DataView.prototype, 'getInt32', {value: makeGetter(Int32Array)}); + Object.defineProperty(DataView.prototype, 'getFloat32', {value: makeGetter(Float32Array)}); + Object.defineProperty(DataView.prototype, 'getFloat64', {value: makeGetter(Float64Array)}); + + function makeSetter(arrayType) { + return function SetViewValue(byteOffset, value, littleEndian) { + byteOffset = ToUint32(byteOffset); + if (byteOffset + arrayType.BYTES_PER_ELEMENT > this.byteLength) + throw RangeError('Array index out of range'); + + // Get bytes + var typeArray = new arrayType([value]), + byteArray = new Uint8Array(typeArray.buffer), + bytes = [], i, byteView; + + for (i = 0; i < arrayType.BYTES_PER_ELEMENT; i += 1) + bytes.push(r(byteArray, i)); + + // Flip if necessary + if (Boolean(littleEndian) === Boolean(IS_BIG_ENDIAN)) + bytes.reverse(); + + // Write them + byteView = new Uint8Array(this.buffer, byteOffset, arrayType.BYTES_PER_ELEMENT); + byteView.set(bytes); + }; + } + + Object.defineProperty(DataView.prototype, 'setUint8', {value: makeSetter(Uint8Array)}); + Object.defineProperty(DataView.prototype, 'setInt8', {value: makeSetter(Int8Array)}); + Object.defineProperty(DataView.prototype, 'setUint16', {value: makeSetter(Uint16Array)}); + Object.defineProperty(DataView.prototype, 'setInt16', {value: makeSetter(Int16Array)}); + Object.defineProperty(DataView.prototype, 'setUint32', {value: makeSetter(Uint32Array)}); + Object.defineProperty(DataView.prototype, 'setInt32', {value: makeSetter(Int32Array)}); + Object.defineProperty(DataView.prototype, 'setFloat32', {value: makeSetter(Float32Array)}); + Object.defineProperty(DataView.prototype, 'setFloat64', {value: makeSetter(Float64Array)}); + + global.DataView = global.DataView || DataView; + + }()); + +}(this)); diff --git a/rockworkd/libpebble/musicendpoint.cpp b/rockworkd/libpebble/musicendpoint.cpp new file mode 100644 index 0000000..f66afda --- /dev/null +++ b/rockworkd/libpebble/musicendpoint.cpp @@ -0,0 +1,63 @@ +#include "musicendpoint.h" +#include "pebble.h" +#include "watchconnection.h" + +#include <QDebug> + +MusicEndpoint::MusicEndpoint(Pebble *pebble, WatchConnection *connection): + QObject(pebble), + m_pebble(pebble), + m_watchConnection(connection) +{ + m_watchConnection->registerEndpointHandler(WatchConnection::EndpointMusicControl, this, "handleMessage"); +} + +void MusicEndpoint::setMusicMetadata(const MusicMetaData &metaData) +{ + m_metaData = metaData; + writeMetadata(); +} + +void MusicEndpoint::writeMetadata() +{ + if (!m_watchConnection->isConnected()) { + return; + } + QStringList tmp; + tmp.append(m_metaData.artist.left(30)); + tmp.append(m_metaData.album.left(30)); + tmp.append(m_metaData.title.left(30)); + QByteArray res = m_watchConnection->buildMessageData(16, tmp); // Not yet sure what the 16 is about :/ + + m_watchConnection->writeToPebble(WatchConnection::EndpointMusicControl, res); +} + +void MusicEndpoint::handleMessage(const QByteArray &data) +{ + MusicControlButton controlButton; + switch (data.toHex().toInt()) { + case 0x01: + controlButton = MusicControlPlayPause; + break; + case 0x04: + controlButton = MusicControlSkipNext; + break; + case 0x05: + controlButton = MusicControlSkipBack; + break; + case 0x06: + controlButton = MusicControlVolumeUp; + break; + case 0x07: + controlButton = MusicControlVolumeDown; + break; + case 0x08: + writeMetadata(); + return; + default: + qWarning() << "Unhandled music control button pressed:" << data.toHex(); + return; + } + emit musicControlPressed(controlButton); +} + diff --git a/rockworkd/libpebble/musicendpoint.h b/rockworkd/libpebble/musicendpoint.h new file mode 100644 index 0000000..1978e46 --- /dev/null +++ b/rockworkd/libpebble/musicendpoint.h @@ -0,0 +1,37 @@ +#ifndef MUSICENDPOINT_H +#define MUSICENDPOINT_H + +#include "musicmetadata.h" +#include "enums.h" + +#include <QObject> + +class Pebble; +class WatchConnection; + +class MusicEndpoint : public QObject +{ + Q_OBJECT +public: + explicit MusicEndpoint(Pebble *pebble, WatchConnection *connection); + +public slots: + void setMusicMetadata(const MusicMetaData &metaData); + +private slots: + void handleMessage(const QByteArray &data); + +signals: + void musicControlPressed(MusicControlButton button); + +private: + void writeMetadata(); + +private: + Pebble *m_pebble; + WatchConnection *m_watchConnection; + + MusicMetaData m_metaData; +}; + +#endif // MUSICENDPOINT_H diff --git a/rockworkd/libpebble/musicmetadata.cpp b/rockworkd/libpebble/musicmetadata.cpp new file mode 100644 index 0000000..fd40a2a --- /dev/null +++ b/rockworkd/libpebble/musicmetadata.cpp @@ -0,0 +1,14 @@ +#include "musicmetadata.h" + +MusicMetaData::MusicMetaData() +{ + +} + +MusicMetaData::MusicMetaData(const QString &artist, const QString &album, const QString &title): + artist(artist), + album(album), + title(title) +{ + +} diff --git a/rockworkd/libpebble/musicmetadata.h b/rockworkd/libpebble/musicmetadata.h new file mode 100644 index 0000000..d40872c --- /dev/null +++ b/rockworkd/libpebble/musicmetadata.h @@ -0,0 +1,17 @@ +#ifndef MUSICMETADATA_H +#define MUSICMETADATA_H + +#include <QString> + +class MusicMetaData +{ +public: + MusicMetaData(); + MusicMetaData(const QString &artist, const QString &album, const QString &title); + + QString artist; + QString album; + QString title; +}; + +#endif // MUSICMETADATA_H diff --git a/rockworkd/libpebble/notification.cpp b/rockworkd/libpebble/notification.cpp new file mode 100644 index 0000000..4b149b8 --- /dev/null +++ b/rockworkd/libpebble/notification.cpp @@ -0,0 +1,79 @@ +#include "notification.h" + +Notification::Notification(const QString &sourceId) : + m_sourceId(sourceId) +{ + +} + +QString Notification::sourceId() const +{ + return m_sourceId; +} + +void Notification::setSourceId(const QString &sourceId) +{ + m_sourceId = sourceId; +} + +QString Notification::sourceName() const +{ + return m_sourceName; +} + +void Notification::setSourceName(const QString &sourceName) +{ + m_sourceName = sourceName; +} + +QString Notification::sender() const +{ + return m_sender; +} + +void Notification::setSender(const QString &sender) +{ + m_sender = sender; +} + +QString Notification::subject() const +{ + return m_subject; +} + +void Notification::setSubject(const QString &subject) +{ + m_subject = subject; +} + +QString Notification::body() const +{ + return m_body; +} + +void Notification::setBody(const QString &body) +{ + m_body = body; +} + +Notification::NotificationType Notification::type() const +{ + return m_type; +} + +void Notification::setType(Notification::NotificationType type) +{ + m_type = type; +} + +QString Notification::actToken() const +{ + return m_actToken; +} + +void Notification::setActToken(QString actToken) +{ + m_actToken = actToken; +} + + diff --git a/rockworkd/libpebble/notification.h b/rockworkd/libpebble/notification.h new file mode 100644 index 0000000..1ab76a0 --- /dev/null +++ b/rockworkd/libpebble/notification.h @@ -0,0 +1,59 @@ +#ifndef NOTIFICATION_H +#define NOTIFICATION_H + +#include <QString> + +class Notification +{ +public: + enum NotificationType { + NotificationTypeGeneric, + NotificationTypeEmail, + NotificationTypeSMS, + NotificationTypeFacebook, + NotificationTypeTwitter, + NotificationTypeTelegram, + NotificationTypeWhatsApp, + NotificationTypeHangout, + NotificationTypeGMail, + NotificationTypeWeather, + NotificationTypeMusic, + NotificationTypeMissedCall, + NotificationTypeAlarm, + NotificationTypeReminder, + }; + + Notification(const QString &sourceId = QString()); + + QString sourceId() const; + void setSourceId(const QString &sourceId); + + QString sourceName() const; + void setSourceName(const QString &sourceName); + + QString sender() const; + void setSender(const QString &sender); + + QString subject() const; + void setSubject(const QString &subject); + + QString body() const; + void setBody(const QString &body); + + NotificationType type() const; + void setType(NotificationType type); + + QString actToken() const; + void setActToken(QString actToken); + +private: + QString m_sourceId; + QString m_sourceName; + QString m_sender; + QString m_subject; + QString m_body; + NotificationType m_type = NotificationTypeGeneric; + QString m_actToken; +}; + +#endif // NOTIFICATION_H diff --git a/rockworkd/libpebble/notificationendpoint.cpp b/rockworkd/libpebble/notificationendpoint.cpp new file mode 100644 index 0000000..363563a --- /dev/null +++ b/rockworkd/libpebble/notificationendpoint.cpp @@ -0,0 +1,46 @@ +#include "notificationendpoint.h" + +#include "watchconnection.h" +#include "pebble.h" +#include "blobdb.h" + +#include <QDebug> +#include <QDateTime> + +NotificationEndpoint::NotificationEndpoint(Pebble *pebble, WatchConnection *watchConnection): + QObject(pebble), + m_pebble(pebble), + m_watchConnection(watchConnection) +{ +} + +void NotificationEndpoint::sendLegacyNotification(const Notification ¬ification) +{ + LegacyNotification::Source source = LegacyNotification::SourceSMS; + switch (notification.type()) { + case Notification::NotificationTypeEmail: + source = LegacyNotification::SourceEmail; + break; + case Notification::NotificationTypeFacebook: + source = LegacyNotification::SourceFacebook; + break; + case Notification::NotificationTypeSMS: + source = LegacyNotification::SourceSMS; + break; + case Notification::NotificationTypeTwitter: + source = LegacyNotification::SourceTwitter; + break; + default: + source = LegacyNotification::SourceSMS; + } + + QString body = notification.subject().isEmpty() ? notification.body() : notification.subject(); + LegacyNotification legacyNotification(source, notification.sender(), body, QDateTime::currentDateTime(), notification.subject()); + m_watchConnection->writeToPebble(WatchConnection::EndpointNotification, legacyNotification.serialize()); +} + +void NotificationEndpoint::notificationReply(const QByteArray &data) +{ + qDebug() << "have notification reply" << data.toHex(); + +} diff --git a/rockworkd/libpebble/notificationendpoint.h b/rockworkd/libpebble/notificationendpoint.h new file mode 100644 index 0000000..211c8cd --- /dev/null +++ b/rockworkd/libpebble/notificationendpoint.h @@ -0,0 +1,64 @@ +#include <QObject> +#include <QUuid> +#include <QDateTime> + +#include "pebble.h" +#include "watchconnection.h" + +class LegacyNotification: public PebblePacket +{ +// class Meta: +// endpoint = 3000 +// endianness = '<' +public: + enum Source { + SourceEmail = 0, + SourceSMS = 1, + SourceFacebook = 2, + SourceTwitter = 3 + }; + + LegacyNotification(Source source, const QString &sender, const QString &body, const QDateTime ×tamp, const QString &subject): + PebblePacket(), + m_source(source), + m_sender(sender), + m_body(body), + m_timestamp(timestamp), + m_subject(subject) + {} + + QByteArray serialize() const override + { + QByteArray ret; + ret.append((quint8)m_source); + ret.append(packString(m_sender)); + ret.append(packString(m_body)); + ret.append(packString(QString::number(m_timestamp.toMSecsSinceEpoch()))); + ret.append(packString(m_subject)); + return ret; + } + +private: + + Source m_source; // uint8 + QString m_sender; + QString m_body; + QDateTime m_timestamp; + QString m_subject; +}; + +class NotificationEndpoint: public QObject +{ + Q_OBJECT +public: + NotificationEndpoint(Pebble *pebble, WatchConnection *watchConnection); + + void sendLegacyNotification(const Notification ¬ification); + +private slots: + void notificationReply(const QByteArray &data); + +private: + Pebble *m_pebble; + WatchConnection *m_watchConnection; +}; diff --git a/rockworkd/libpebble/pebble.cpp b/rockworkd/libpebble/pebble.cpp new file mode 100644 index 0000000..5655cc7 --- /dev/null +++ b/rockworkd/libpebble/pebble.cpp @@ -0,0 +1,693 @@ +#include "pebble.h" +#include "watchconnection.h" +#include "notificationendpoint.h" +#include "watchdatareader.h" +#include "watchdatawriter.h" +#include "musicendpoint.h" +#include "phonecallendpoint.h" +#include "appmanager.h" +#include "appmsgmanager.h" +#include "jskit/jskitmanager.h" +#include "blobdb.h" +#include "appdownloader.h" +#include "screenshotendpoint.h" +#include "firmwaredownloader.h" +#include "watchlogendpoint.h" +#include "core.h" +#include "platforminterface.h" +#include "ziphelper.h" +#include "dataloggingendpoint.h" + +#include "QDir" +#include <QDateTime> +#include <QStandardPaths> +#include <QSettings> +#include <QTimeZone> + +Pebble::Pebble(const QBluetoothAddress &address, QObject *parent): + QObject(parent), + m_address(address) +{ + m_storagePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + m_address.toString().replace(':', '_') + "/"; + + m_connection = new WatchConnection(this); + QObject::connect(m_connection, &WatchConnection::watchConnected, this, &Pebble::onPebbleConnected); + QObject::connect(m_connection, &WatchConnection::watchDisconnected, this, &Pebble::onPebbleDisconnected); + + m_connection->registerEndpointHandler(WatchConnection::EndpointVersion, this, "pebbleVersionReceived"); + m_connection->registerEndpointHandler(WatchConnection::EndpointPhoneVersion, this, "phoneVersionAsked"); + m_connection->registerEndpointHandler(WatchConnection::EndpointFactorySettings, this, "factorySettingsReceived"); + + m_dataLogEndpoint = new DataLoggingEndpoint(this, m_connection); + + m_notificationEndpoint = new NotificationEndpoint(this, m_connection); + QObject::connect(Core::instance()->platform(), &PlatformInterface::notificationReceived, this, &Pebble::sendNotification); + + m_musicEndpoint = new MusicEndpoint(this, m_connection); + m_musicEndpoint->setMusicMetadata(Core::instance()->platform()->musicMetaData()); + QObject::connect(m_musicEndpoint, &MusicEndpoint::musicControlPressed, Core::instance()->platform(), &PlatformInterface::sendMusicControlCommand); + QObject::connect(Core::instance()->platform(), &PlatformInterface::musicMetadataChanged, m_musicEndpoint, &MusicEndpoint::setMusicMetadata); + + m_phoneCallEndpoint = new PhoneCallEndpoint(this, m_connection); + QObject::connect(m_phoneCallEndpoint, &PhoneCallEndpoint::hangupCall, Core::instance()->platform(), &PlatformInterface::hangupCall); + QObject::connect(Core::instance()->platform(), &PlatformInterface::incomingCall, m_phoneCallEndpoint, &PhoneCallEndpoint::incomingCall); + QObject::connect(Core::instance()->platform(), &PlatformInterface::callStarted, m_phoneCallEndpoint, &PhoneCallEndpoint::callStarted); + QObject::connect(Core::instance()->platform(), &PlatformInterface::callEnded, m_phoneCallEndpoint, &PhoneCallEndpoint::callEnded); + + m_appManager = new AppManager(this, m_connection); + QObject::connect(m_appManager, &AppManager::appsChanged, this, &Pebble::installedAppsChanged); + QObject::connect(m_appManager, &AppManager::idMismatchDetected, this, &Pebble::resetPebble); + + m_appMsgManager = new AppMsgManager(this, m_appManager, m_connection); + m_jskitManager = new JSKitManager(this, m_connection, m_appManager, m_appMsgManager, this); + QObject::connect(m_jskitManager, SIGNAL(openURL(const QString&, const QString&)), this, SIGNAL(openURL(const QString&, const QString&))); + + m_blobDB = new BlobDB(this, m_connection); + QObject::connect(m_blobDB, &BlobDB::muteSource, this, &Pebble::muteNotificationSource); + QObject::connect(m_blobDB, &BlobDB::actionTriggered, Core::instance()->platform(), &PlatformInterface::actionTriggered); + QObject::connect(m_blobDB, &BlobDB::appInserted, this, &Pebble::appInstalled); + QObject::connect(Core::instance()->platform(), &PlatformInterface::organizerItemsChanged, this, &Pebble::syncCalendar); + + m_appDownloader = new AppDownloader(m_storagePath, this); + QObject::connect(m_appDownloader, &AppDownloader::downloadFinished, this, &Pebble::appDownloadFinished); + + m_screenshotEndpoint = new ScreenshotEndpoint(this, m_connection, this); + QObject::connect(m_screenshotEndpoint, &ScreenshotEndpoint::screenshotAdded, this, &Pebble::screenshotAdded); + QObject::connect(m_screenshotEndpoint, &ScreenshotEndpoint::screenshotRemoved, this, &Pebble::screenshotRemoved); + + m_firmwareDownloader = new FirmwareDownloader(this, m_connection); + QObject::connect(m_firmwareDownloader, &FirmwareDownloader::updateAvailableChanged, this, &Pebble::slotUpdateAvailableChanged); + QObject::connect(m_firmwareDownloader, &FirmwareDownloader::upgradingChanged, this, &Pebble::upgradingFirmwareChanged); + + m_logEndpoint = new WatchLogEndpoint(this, m_connection); + QObject::connect(m_logEndpoint, &WatchLogEndpoint::logsFetched, this, &Pebble::logsDumped); + + QSettings watchInfo(m_storagePath + "/watchinfo.conf", QSettings::IniFormat); + m_model = (Model)watchInfo.value("watchModel", (int)ModelUnknown).toInt(); + + QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat); + settings.beginGroup("activityParams"); + m_healthParams.setEnabled(settings.value("enabled").toBool()); + m_healthParams.setAge(settings.value("age").toUInt()); + m_healthParams.setHeight(settings.value("height").toInt()); + m_healthParams.setGender((HealthParams::Gender)settings.value("gender").toInt()); + m_healthParams.setWeight(settings.value("weight").toInt()); + m_healthParams.setMoreActive(settings.value("moreActive").toBool()); + m_healthParams.setSleepMore(settings.value("sleepMore").toBool()); + settings.endGroup(); + + settings.beginGroup("unitsDistance"); + m_imperialUnits = settings.value("imperialUnits", false).toBool(); + settings.endGroup(); + + settings.beginGroup("calendar"); + m_calendarSyncEnabled = settings.value("calendarSyncEnabled", true).toBool(); + settings.endGroup(); +} + +QBluetoothAddress Pebble::address() const +{ + return m_address; +} + +QString Pebble::name() const +{ + return m_name; +} + +void Pebble::setName(const QString &name) +{ + m_name = name; +} + +QBluetoothLocalDevice::Pairing Pebble::pairingStatus() const +{ + QBluetoothLocalDevice dev; + return dev.pairingStatus(m_address); +} + +bool Pebble::connected() const +{ + return m_connection->isConnected() && !m_serialNumber.isEmpty(); +} + +void Pebble::connect() +{ + qDebug() << "Connecting to Pebble:" << m_name << m_address; + m_connection->connectPebble(m_address); +} + +QDateTime Pebble::softwareBuildTime() const +{ + return m_softwareBuildTime; +} + +QString Pebble::softwareVersion() const +{ + return m_softwareVersion; +} + +QString Pebble::softwareCommitRevision() const +{ + return m_softwareCommitRevision; +} + +HardwareRevision Pebble::hardwareRevision() const +{ + return m_hardwareRevision; +} + +Model Pebble::model() const +{ + return m_model; +} + +void Pebble::setHardwareRevision(HardwareRevision hardwareRevision) +{ + m_hardwareRevision = hardwareRevision; + switch (m_hardwareRevision) { + case HardwareRevisionUNKNOWN: + m_hardwarePlatform = HardwarePlatformUnknown; + break; + case HardwareRevisionTINTIN_EV1: + case HardwareRevisionTINTIN_EV2: + case HardwareRevisionTINTIN_EV2_3: + case HardwareRevisionTINTIN_EV2_4: + case HardwareRevisionTINTIN_V1_5: + case HardwareRevisionBIANCA: + case HardwareRevisionTINTIN_BB: + case HardwareRevisionTINTIN_BB2: + m_hardwarePlatform = HardwarePlatformAplite; + break; + case HardwareRevisionSNOWY_EVT2: + case HardwareRevisionSNOWY_DVT: + case HardwareRevisionBOBBY_SMILES: + case HardwareRevisionSNOWY_BB: + case HardwareRevisionSNOWY_BB2: + m_hardwarePlatform = HardwarePlatformBasalt; + break; + case HardwareRevisionSPALDING_EVT: + case HardwareRevisionSPALDING: + case HardwareRevisionSPALDING_BB2: + m_hardwarePlatform = HardwarePlatformChalk; + break; + } +} + +HardwarePlatform Pebble::hardwarePlatform() const +{ + return m_hardwarePlatform; +} + +QString Pebble::serialNumber() const +{ + return m_serialNumber; +} + +QString Pebble::language() const +{ + return m_language; +} + +Capabilities Pebble::capabilities() const +{ + return m_capabilities; +} + +bool Pebble::isUnfaithful() const +{ + return m_isUnfaithful; +} + +bool Pebble::recovery() const +{ + return m_recovery; +} + +bool Pebble::upgradingFirmware() const +{ + return m_firmwareDownloader->upgrading(); +} + +void Pebble::setHealthParams(const HealthParams &healthParams) +{ + m_healthParams = healthParams; + m_blobDB->setHealthParams(healthParams); + emit healtParamsChanged(); + + QSettings healthSettings(m_storagePath + "/appsettings.conf", QSettings::IniFormat); + healthSettings.beginGroup("activityParams"); + healthSettings.setValue("enabled", m_healthParams.enabled()); + healthSettings.setValue("age", m_healthParams.age()); + healthSettings.setValue("height", m_healthParams.height()); + healthSettings.setValue("gender", m_healthParams.gender()); + healthSettings.setValue("weight", m_healthParams.weight()); + healthSettings.setValue("moreActive", m_healthParams.moreActive()); + healthSettings.setValue("sleepMore", m_healthParams.sleepMore()); + +} + +HealthParams Pebble::healthParams() const +{ + return m_healthParams; +} + +void Pebble::setImperialUnits(bool imperial) +{ + m_imperialUnits = imperial; + m_blobDB->setUnits(imperial); + emit imperialUnitsChanged(); + + QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat); + settings.beginGroup("unitsDistance"); + settings.setValue("enabled", m_imperialUnits); +} + +bool Pebble::imperialUnits() const +{ + return m_imperialUnits; +} + +void Pebble::dumpLogs(const QString &fileName) const +{ + m_logEndpoint->fetchLogs(fileName); +} + +QString Pebble::storagePath() const +{ + return m_storagePath; +} + +QHash<QString, bool> Pebble::notificationsFilter() const +{ + QHash<QString, bool> ret; + QString settingsFile = m_storagePath + "/notifications.conf"; + QSettings s(settingsFile, QSettings::IniFormat); + foreach (const QString &key, s.allKeys()) { + ret.insert(key, s.value(key).toBool()); + } + return ret; +} + +void Pebble::setNotificationFilter(const QString &sourceId, bool enabled) +{ + QString settingsFile = m_storagePath + "/notifications.conf"; + QSettings s(settingsFile, QSettings::IniFormat); + if (!s.contains(sourceId) || s.value(sourceId).toBool() != enabled) { + s.setValue(sourceId, enabled); + emit notificationFilterChanged(sourceId, enabled); + } +} + +void Pebble::sendNotification(const Notification ¬ification) +{ + if (!notificationsFilter().value(notification.sourceId(), true)) { + qDebug() << "Notifications for" << notification.sourceId() << "disabled."; + return; + } + // In case it wasn't there before, make sure to write it to the config now so it will appear in the config app. + setNotificationFilter(notification.sourceId(), true); + + qDebug() << "Sending notification from source" << notification.sourceId() << "to watch"; + + if (m_softwareVersion < "v3.0") { + m_notificationEndpoint->sendLegacyNotification(notification); + } else { + m_blobDB->insertNotification(notification); + } +} + +void Pebble::clearAppDB() +{ + m_blobDB->clearApps(); +} + +void Pebble::clearTimeline() +{ + m_blobDB->clearTimeline(); +} + +void Pebble::setCalendarSyncEnabled(bool enabled) +{ + if (m_calendarSyncEnabled == enabled) { + return; + } + m_calendarSyncEnabled = enabled; + emit calendarSyncEnabledChanged(); + + if (!m_calendarSyncEnabled) { + m_blobDB->clearTimeline(); + } else { + syncCalendar(Core::instance()->platform()->organizerItems()); + } + + QSettings settings(m_storagePath + "/appsettings.conf", QSettings::IniFormat); + settings.beginGroup("calendar"); + settings.setValue("calendarSyncEnabled", m_calendarSyncEnabled); + settings.endGroup(); +} + +bool Pebble::calendarSyncEnabled() const +{ + return m_calendarSyncEnabled; +} + +void Pebble::syncCalendar(const QList<CalendarEvent> &items) +{ + if (connected() && m_calendarSyncEnabled) { + m_blobDB->syncCalendar(items); + } +} + +void Pebble::installApp(const QString &id) +{ + m_appDownloader->downloadApp(id); +} + +void Pebble::sideloadApp(const QString &packageFile) +{ + QString targetFile = packageFile; + targetFile.remove("file://"); + + QString id; + int i = 0; + do { + QDir dir(m_storagePath + "/apps/sideload" + QString::number(i)); + if (!dir.exists()) { + if (!dir.mkpath(dir.absolutePath())) { + qWarning() << "Error creating dir for unpacking. Cannot install package" << packageFile; + return; + } + id = "sideload" + QString::number(i); + } + i++; + } while (id.isEmpty()); + + if (!ZipHelper::unpackArchive(targetFile, m_storagePath + "/apps/" + id)) { + qWarning() << "Error unpacking App zip file" << targetFile << "to" << m_storagePath + "/apps/" + id; + return; + } + + qDebug() << "Sideload package unpacked."; + appDownloadFinished(id); +} + +QList<QUuid> Pebble::installedAppIds() +{ + return m_appManager->appUuids(); +} + +void Pebble::setAppOrder(const QList<QUuid> &newList) +{ + m_appManager->setAppOrder(newList); +} + +AppInfo Pebble::appInfo(const QUuid &uuid) +{ + return m_appManager->info(uuid); +} + +void Pebble::removeApp(const QUuid &uuid) +{ + qDebug() << "Should remove app:" << uuid; + m_blobDB->removeApp(m_appManager->info(uuid)); + m_appManager->removeApp(uuid); +} + +void Pebble::launchApp(const QUuid &uuid) +{ + m_appMsgManager->launchApp(uuid); +} + +void Pebble::requestConfigurationURL(const QUuid &uuid) { + if (m_jskitManager->currentApp().uuid() == uuid) { + m_jskitManager->showConfiguration(); + } + else { + m_jskitManager->setConfigurationId(uuid); + m_appMsgManager->launchApp(uuid); + } +} + +void Pebble::configurationClosed(const QUuid &uuid, const QString &result) +{ + if (m_jskitManager->currentApp().uuid() == uuid) { + m_jskitManager->handleWebviewClosed(result); + } +} + +void Pebble::requestScreenshot() +{ + m_screenshotEndpoint->requestScreenshot(); +} + +QStringList Pebble::screenshots() const +{ + return m_screenshotEndpoint->screenshots(); +} + +void Pebble::removeScreenshot(const QString &filename) +{ + m_screenshotEndpoint->removeScreenshot(filename); +} + +bool Pebble::firmwareUpdateAvailable() const +{ + return m_firmwareDownloader->updateAvailable(); +} + +QString Pebble::candidateFirmwareVersion() const +{ + return m_firmwareDownloader->candidateVersion(); +} + +QString Pebble::firmwareReleaseNotes() const +{ + return m_firmwareDownloader->releaseNotes(); +} + +void Pebble::upgradeFirmware() const +{ + m_firmwareDownloader->performUpgrade(); +} + +void Pebble::onPebbleConnected() +{ + qDebug() << "Pebble connected:" << m_name; + QByteArray data; + WatchDataWriter w(&data); + w.write<quint8>(0); // Command fetch + QString message = "mfg_color"; + w.writeLE<quint8>(message.length()); + w.writeFixedString(message.length(), message); + m_connection->writeToPebble(WatchConnection::EndpointFactorySettings, data); + + m_connection->writeToPebble(WatchConnection::EndpointVersion, QByteArray(1, 0)); +} + +void Pebble::onPebbleDisconnected() +{ + qDebug() << "Pebble disconnected:" << m_name; + emit pebbleDisconnected(); +} + +void Pebble::pebbleVersionReceived(const QByteArray &data) +{ + WatchDataReader wd(data); + + wd.skip(1); + m_softwareBuildTime = QDateTime::fromTime_t(wd.read<quint32>()); + qDebug() << "Software Version build:" << m_softwareBuildTime; + m_softwareVersion = wd.readFixedString(32); + qDebug() << "Software Version string:" << m_softwareVersion; + m_softwareCommitRevision = wd.readFixedString(8); + qDebug() << "Software Version commit:" << m_softwareCommitRevision; + + m_recovery = wd.read<quint8>(); + qDebug() << "Recovery:" << m_recovery; + HardwareRevision rev = (HardwareRevision)wd.read<quint8>(); + setHardwareRevision(rev); + qDebug() << "HW Revision:" << rev; + qDebug() << "Metadata Version:" << wd.read<quint8>(); + + qDebug() << "Safe build:" << QDateTime::fromTime_t(wd.read<quint32>()); + qDebug() << "Safe version:" << wd.readFixedString(32); + qDebug() << "safe commit:" << wd.readFixedString(8); + qDebug() << "Safe recovery:" << wd.read<quint8>(); + qDebug() << "HW Revision:" << wd.read<quint8>(); + qDebug() << "Metadata Version:" << wd.read<quint8>(); + + qDebug() << "BootloaderBuild" << QDateTime::fromTime_t(wd.read<quint32>()); + qDebug() << "hardwareRevision" << wd.readFixedString(9); + m_serialNumber = wd.readFixedString(12); + qDebug() << "serialnumber" << m_serialNumber; + qDebug() << "BT address" << wd.readBytes(6).toHex(); + qDebug() << "CRC:" << wd.read<quint32>(); + qDebug() << "Resource timestamp:" << QDateTime::fromTime_t(wd.read<quint32>()); + m_language = wd.readFixedString(6); + qDebug() << "Language" << m_language; + qDebug() << "Language version" << wd.read<quint16>(); + // Capabilities is 64 bits but QFlags can only do 32 bits. lets split it into 2 * 32. + // only 8 bits are used atm anyways. + m_capabilities = QFlag(wd.readLE<quint32>()); + qDebug() << "Capabilities" << QString::number(m_capabilities, 16); + qDebug() << "Capabilities" << wd.readLE<quint32>(); + m_isUnfaithful = wd.read<quint8>(); + qDebug() << "Is Unfaithful" << m_isUnfaithful; + + // This is useful for debugging + //m_isUnfaithful = true; + + if (!m_recovery) { + m_appManager->rescan(); + + QSettings version(m_storagePath + "/watchinfo.conf", QSettings::IniFormat); + if (version.value("syncedWithVersion").toString() != QStringLiteral(VERSION)) { + m_isUnfaithful = true; + } + + if (m_isUnfaithful) { + qDebug() << "Pebble sync state unclear. Resetting Pebble watch."; + resetPebble(); + } else { + syncCalendar(Core::instance()->platform()->organizerItems()); + syncApps(); + m_blobDB->setHealthParams(m_healthParams); + m_blobDB->setUnits(m_imperialUnits); + } + version.setValue("syncedWithVersion", QStringLiteral(VERSION)); + + syncTime(); + } + + m_firmwareDownloader->checkForNewFirmware(); + emit pebbleConnected(); + +} + +void Pebble::factorySettingsReceived(const QByteArray &data) +{ + qDebug() << "have factory settings" << data.toHex(); + + WatchDataReader reader(data); + quint8 status = reader.read<quint8>(); + quint8 len = reader.read<quint8>(); + + if (status != 0x01 && len != 0x04) { + qWarning() << "Unexpected data reading factory settings"; + return; + } + m_model = (Model)reader.read<quint32>(); + QSettings s(m_storagePath + "/watchinfo.conf", QSettings::IniFormat); + s.setValue("watchModel", m_model); +} + +void Pebble::phoneVersionAsked(const QByteArray &data) +{ + + QByteArray res; + + Capabilities sessionCap(CapabilityHealth + | CapabilityAppRunState + | CapabilityUpdatedMusicProtocol | CapabilityInfiniteLogDumping | Capability8kAppMessages); + + quint32 platformFlags = 16 | 32 | OSAndroid; + + WatchDataWriter writer(&res); + writer.writeLE<quint8>(0x01); // ok + writer.writeLE<quint32>(0xFFFFFFFF); + writer.writeLE<quint32>(sessionCap); + writer.write<quint32>(platformFlags); + writer.write<quint8>(2); // response version + writer.write<quint8>(3); // major version + writer.write<quint8>(0); // minor version + writer.write<quint8>(0); // bugfix version + writer.writeLE<quint64>(sessionCap); + + qDebug() << "sending phone version" << res.toHex(); + + m_connection->writeToPebble(WatchConnection::EndpointPhoneVersion, res); +} + +void Pebble::appDownloadFinished(const QString &id) +{ + QUuid uuid = m_appManager->scanApp(m_storagePath + "/apps/" + id); + if (uuid.isNull()) { + qWarning() << "Error scanning downloaded app. Won't install on watch"; + return; + } + m_blobDB->insertAppMetaData(m_appManager->info(uuid)); + m_pendingInstallations.append(uuid); +} + +void Pebble::appInstalled(const QUuid &uuid) { + if (m_pendingInstallations.contains(uuid)) { + m_appMsgManager->launchApp(uuid); + } +} + +void Pebble::muteNotificationSource(const QString &source) +{ + setNotificationFilter(source, false); +} + +void Pebble::resetPebble() +{ + clearTimeline(); + syncCalendar(Core::instance()->platform()->organizerItems()); + + clearAppDB(); + syncApps(); +} + +void Pebble::syncApps() +{ + foreach (const QUuid &appUuid, m_appManager->appUuids()) { + if (!m_appManager->info(appUuid).isSystemApp()) { + qDebug() << "Inserting app" << m_appManager->info(appUuid).shortName() << "into BlobDB"; + m_blobDB->insertAppMetaData(m_appManager->info(appUuid)); + } + } + // make sure the order is synced too + m_appManager->setAppOrder(m_appManager->appUuids()); +} + +void Pebble::syncTime() +{ + TimeMessage msg(TimeMessage::TimeOperationSetUTC); + qDebug() << "Syncing Time" << QDateTime::currentDateTime() << msg.serialize().toHex(); + m_connection->writeToPebble(WatchConnection::EndpointTime, msg.serialize()); +} + +void Pebble::slotUpdateAvailableChanged() +{ + qDebug() << "update available" << m_firmwareDownloader->updateAvailable() << m_firmwareDownloader->candidateVersion(); + + emit updateAvailableChanged(); +} + + +TimeMessage::TimeMessage(TimeMessage::TimeOperation operation) : + m_operation(operation) +{ + +} +QByteArray TimeMessage::serialize() const +{ + QByteArray ret; + WatchDataWriter writer(&ret); + writer.write<quint8>(m_operation); + switch (m_operation) { + case TimeOperationSetLocaltime: + writer.writeLE<quint32>(QDateTime::currentMSecsSinceEpoch() / 1000); + break; + case TimeOperationSetUTC: + writer.write<quint32>(QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000); + writer.write<qint16>(QDateTime::currentDateTime().offsetFromUtc() / 60); + writer.writePascalString(QDateTime::currentDateTime().timeZone().displayName(QTimeZone::StandardTime)); + break; + default: + ; + } + return ret; +} diff --git a/rockworkd/libpebble/pebble.h b/rockworkd/libpebble/pebble.h new file mode 100644 index 0000000..8650d74 --- /dev/null +++ b/rockworkd/libpebble/pebble.h @@ -0,0 +1,225 @@ +#ifndef PEBBLE_H +#define PEBBLE_H + +#include "musicmetadata.h" +#include "notification.h" +#include "calendarevent.h" +#include "appinfo.h" +#include "healthparams.h" + +#include <QObject> +#include <QBluetoothAddress> +#include <QBluetoothLocalDevice> +#include <QTimer> + +class WatchConnection; +class NotificationEndpoint; +class MusicEndpoint; +class PhoneCallEndpoint; +class AppManager; +class AppMsgManager; +class BankManager; +class JSKitManager; +class BlobDB; +class AppDownloader; +class ScreenshotEndpoint; +class FirmwareDownloader; +class WatchLogEndpoint; +class DataLoggingEndpoint; + +class Pebble : public QObject +{ + Q_OBJECT + Q_ENUMS(Pebble::NotificationType) + Q_PROPERTY(QBluetoothAddress address MEMBER m_address) + Q_PROPERTY(QString name MEMBER m_name) + Q_PROPERTY(HardwareRevision HardwareRevision READ hardwareRevision) + Q_PROPERTY(Model model READ model) + Q_PROPERTY(HardwarePlatform hardwarePlatform MEMBER m_hardwarePlatform) + Q_PROPERTY(QString softwareVersion MEMBER m_softwareVersion) + Q_PROPERTY(QString serialNumber MEMBER m_serialNumber) + Q_PROPERTY(QString language MEMBER m_language) + +public: + explicit Pebble(const QBluetoothAddress &address, QObject *parent = 0); + + QBluetoothAddress address() const; + + QString name() const; + void setName(const QString &name); + + QBluetoothLocalDevice::Pairing pairingStatus() const; + + bool connected() const; + void connect(); + + QDateTime softwareBuildTime() const; + QString softwareVersion() const; + QString softwareCommitRevision() const; + HardwareRevision hardwareRevision() const; + Model model() const; + HardwarePlatform hardwarePlatform() const; + QString serialNumber() const; + QString language() const; + Capabilities capabilities() const; + bool isUnfaithful() const; + bool recovery() const; + + QString storagePath() const; + +public slots: + QHash<QString, bool> notificationsFilter() const; + void setNotificationFilter(const QString &sourceId, bool enabled); + void sendNotification(const Notification ¬ification); + + void clearTimeline(); + void setCalendarSyncEnabled(bool enabled); + bool calendarSyncEnabled() const; + + void clearAppDB(); + void installApp(const QString &id); + void sideloadApp(const QString &packageFile); + QList<QUuid> installedAppIds(); + void setAppOrder(const QList<QUuid> &newList); + AppInfo appInfo(const QUuid &uuid); + void removeApp(const QUuid &uuid); + + void launchApp(const QUuid &uuid); + + void requestConfigurationURL(const QUuid &uuid); + void configurationClosed(const QUuid &uuid, const QString &result); + + void requestScreenshot(); + QStringList screenshots() const; + void removeScreenshot(const QString &filename); + + bool firmwareUpdateAvailable() const; + QString candidateFirmwareVersion() const; + QString firmwareReleaseNotes() const; + void upgradeFirmware() const; + bool upgradingFirmware() const; + + void setHealthParams(const HealthParams &healthParams); + HealthParams healthParams() const; + + void setImperialUnits(bool imperial); + bool imperialUnits() const; + + void dumpLogs(const QString &fileName) const; + +private slots: + void onPebbleConnected(); + void onPebbleDisconnected(); + void pebbleVersionReceived(const QByteArray &data); + void factorySettingsReceived(const QByteArray &data); + void phoneVersionAsked(const QByteArray &data); + void appDownloadFinished(const QString &id); + void appInstalled(const QUuid &uuid); + void muteNotificationSource(const QString &source); + + void resetPebble(); + void syncApps(); + void syncTime(); + void syncCalendar(const QList<CalendarEvent> &items); + + void slotUpdateAvailableChanged(); + +signals: + void pebbleConnected(); + void pebbleDisconnected(); + void notificationFilterChanged(const QString &sourceId, bool enabled); + void musicControlPressed(MusicControlButton control); + void installedAppsChanged(); + void openURL(const QString &uuid, const QString &url); + void screenshotAdded(const QString &filename); + void screenshotRemoved(const QString &filename); + void updateAvailableChanged(); + void upgradingFirmwareChanged(); + void logsDumped(bool success); + + void calendarSyncEnabledChanged(); + void imperialUnitsChanged(); + void healtParamsChanged(); +private: + void setHardwareRevision(HardwareRevision hardwareRevision); + + QBluetoothAddress m_address; + QString m_name; + QDateTime m_softwareBuildTime; + QString m_softwareVersion; + QString m_softwareCommitRevision; + HardwareRevision m_hardwareRevision; + HardwarePlatform m_hardwarePlatform = HardwarePlatformUnknown; + Model m_model = ModelUnknown; + QString m_serialNumber; + QString m_language; + Capabilities m_capabilities = CapabilityNone; + bool m_isUnfaithful = false; + bool m_recovery = false; + + WatchConnection *m_connection; + NotificationEndpoint *m_notificationEndpoint; + MusicEndpoint *m_musicEndpoint; + PhoneCallEndpoint *m_phoneCallEndpoint; + AppManager *m_appManager; + AppMsgManager *m_appMsgManager; + JSKitManager *m_jskitManager; + BankManager *m_bankManager; + BlobDB *m_blobDB; + AppDownloader *m_appDownloader; + ScreenshotEndpoint *m_screenshotEndpoint; + FirmwareDownloader *m_firmwareDownloader; + WatchLogEndpoint *m_logEndpoint; + DataLoggingEndpoint *m_dataLogEndpoint; + + QString m_storagePath; + QList<QUuid> m_pendingInstallations; + + bool m_calendarSyncEnabled = true; + HealthParams m_healthParams; + bool m_imperialUnits = false; +}; + +/* + Capabilities received from phone: + In order, starting at zero, in little-endian (unlike the rest of the messsage), the bits sent by the watch indicate support for: + - app run state, + - infinite log dumping, + - updated music protocol, + - extended notification service, + - language packs, + - 8k app messages, + - health, + - voice + + The capability bits sent *to* the watch are, starting at zero: + - app run state, + - infinite log dumping, + - updated music service, + - extended notification service, + - (unused), + - 8k app messages, + - (unused), + - third-party voice + */ + + + +class TimeMessage: public PebblePacket +{ +public: + enum TimeOperation { + TimeOperationGetRequest = 0x00, + TimeOperationGetResponse = 0x01, + TimeOperationSetLocaltime = 0x02, + TimeOperationSetUTC = 0x03 + }; + TimeMessage(TimeOperation operation); + + QByteArray serialize() const override; + +private: + TimeOperation m_operation = TimeOperationGetRequest; +}; + +#endif // PEBBLE_H diff --git a/rockworkd/libpebble/phonecallendpoint.cpp b/rockworkd/libpebble/phonecallendpoint.cpp new file mode 100644 index 0000000..afd869d --- /dev/null +++ b/rockworkd/libpebble/phonecallendpoint.cpp @@ -0,0 +1,71 @@ +#include "phonecallendpoint.h" + +#include "pebble.h" +#include "watchconnection.h" +#include "watchdatareader.h" + +PhoneCallEndpoint::PhoneCallEndpoint(Pebble *pebble, WatchConnection *connection): + QObject(pebble), + m_pebble(pebble), + m_connection(connection) +{ + m_connection->registerEndpointHandler(WatchConnection::EndpointPhoneControl, this, "handlePhoneEvent"); +} + +void PhoneCallEndpoint::incomingCall(uint cookie, const QString &number, const QString &name) +{ + QStringList tmp; + tmp.append(number); + tmp.append(name); + + char act = CallActionIncoming; + // FIXME: Outgoing calls don't seem to work... Maybe something wrong in the enum? +// if (!incoming) { +// act = CallActionOutgoing; +// } + + phoneControl(act, cookie, tmp); + +} + +void PhoneCallEndpoint::callStarted(uint cookie) +{ + phoneControl(CallActionStart, cookie, QStringList()); +} + +void PhoneCallEndpoint::callEnded(uint cookie, bool missed) +{ + Q_UNUSED(missed) + // FIXME: The watch doesn't seem to react on Missed... So let's always "End" it for now +// phoneControl(missed ? CallActionMissed : CallActionEnd, cookie, QStringList()); + phoneControl(CallActionEnd, cookie, QStringList()); +} + +void PhoneCallEndpoint::phoneControl(char act, uint cookie, QStringList datas) +{ + QByteArray head; + head.append((char)act); + head.append((cookie >> 24)& 0xFF); + head.append((cookie >> 16)& 0xFF); + head.append((cookie >> 8)& 0xFF); + head.append(cookie & 0xFF); + if (datas.length()>0) { + head.append(m_connection->buildData(datas)); + } + + m_connection->writeToPebble(WatchConnection::EndpointPhoneControl, head); +} + +void PhoneCallEndpoint::handlePhoneEvent(const QByteArray &data) +{ + + WatchDataReader reader(data); + reader.skip(1); + uint cookie = reader.read<uint>(); + + if (data.at(0) == CallActionHangup) { + emit hangupCall(cookie); + } else { + qWarning() << "received an unhandled phone event" << data.toHex(); + } +} diff --git a/rockworkd/libpebble/phonecallendpoint.h b/rockworkd/libpebble/phonecallendpoint.h new file mode 100644 index 0000000..994f8a6 --- /dev/null +++ b/rockworkd/libpebble/phonecallendpoint.h @@ -0,0 +1,47 @@ +#ifndef PHONECALLENDPOINT_H +#define PHONECALLENDPOINT_H + +#include <QObject> + +class Pebble; +class WatchConnection; + +class PhoneCallEndpoint : public QObject +{ + Q_OBJECT +public: + enum CallAction{ + CallActionAnswer = 1, + CallActionHangup = 2, + CallActionGetState = 3, + CallActionIncoming = 4, + CallActionOutgoing = 5, + CallActionMissed = 6, + CallActionRing = 7, + CallActionStart = 8, + CallActionEnd = 9 + }; + + explicit PhoneCallEndpoint(Pebble *pebble, WatchConnection *connection); + +public slots: + void incomingCall(uint cookie, const QString &number, const QString &name); + void callStarted(uint cookie); + void callEnded(uint cookie, bool missed); + +signals: + void hangupCall(uint cookie); + + +private: + void phoneControl(char act, uint cookie, QStringList datas); + +private slots: + void handlePhoneEvent(const QByteArray &data); + +private: + Pebble *m_pebble; + WatchConnection *m_connection; +}; + +#endif // PHONECALLENDPOINT_H diff --git a/rockworkd/libpebble/platforminterface.h b/rockworkd/libpebble/platforminterface.h new file mode 100644 index 0000000..6c67598 --- /dev/null +++ b/rockworkd/libpebble/platforminterface.h @@ -0,0 +1,46 @@ +#ifndef PLATFORMINTERFACE_H +#define PLATFORMINTERFACE_H + +#include "libpebble/pebble.h" +#include "libpebble/musicmetadata.h" + +#include <QObject> +#include <QOrganizerItem> + +class PlatformInterface: public QObject +{ + Q_OBJECT +public: + PlatformInterface(QObject *parent = 0): QObject(parent) {} + virtual ~PlatformInterface() {} + +// Notifications +public: + virtual void actionTriggered(const QString &actToken) = 0; +signals: + void notificationReceived(const Notification ¬ification); + +// Music +public: + virtual void sendMusicControlCommand(MusicControlButton controlButton) = 0; + virtual MusicMetaData musicMetaData() const = 0; +signals: + void musicMetadataChanged(MusicMetaData metaData); + +// Phone calls +signals: + void incomingCall(uint cookie, const QString &number, const QString &name); + void callStarted(uint cookie); + void callEnded(uint cookie, bool missed); +public: + virtual void hangupCall(uint cookie) = 0; + +// Organizer +public: + virtual QList<CalendarEvent> organizerItems() const = 0; +signals: + void organizerItemsChanged(const QList<CalendarEvent> &items); + +}; + +#endif // PLATFORMINTERFACE_H diff --git a/rockworkd/libpebble/screenshotendpoint.cpp b/rockworkd/libpebble/screenshotendpoint.cpp new file mode 100644 index 0000000..b31ab70 --- /dev/null +++ b/rockworkd/libpebble/screenshotendpoint.cpp @@ -0,0 +1,131 @@ +#include "screenshotendpoint.h" + +#include "watchdatawriter.h" +#include "watchdatareader.h" +#include "pebble.h" + +#include <QImage> +#include <QDateTime> +#include <QDir> + +ScreenshotEndpoint::ScreenshotEndpoint(Pebble *pebble, WatchConnection *connection, QObject *parent): + QObject(parent), + m_pebble(pebble), + m_connection(connection) +{ + m_connection->registerEndpointHandler(WatchConnection::EndpointScreenshot, this, "handleScreenshotData"); +} + +void ScreenshotEndpoint::requestScreenshot() +{ + ScreenshotRequestPackage package; + m_connection->writeToPebble(WatchConnection::EndpointScreenshot, package.serialize()); +} + +void ScreenshotEndpoint::removeScreenshot(const QString &filename) +{ + QFile f(filename); + if (f.exists() && f.remove()) { + emit screenshotRemoved(filename); + } +} + +QStringList ScreenshotEndpoint::screenshots() const +{ + QDir dir(m_pebble->storagePath() + "/screenshots/"); + QStringList ret; + foreach (const QString &filename, dir.entryList(QDir::Files)) { + ret << m_pebble->storagePath() + "/screenshots/" + filename; + } + + return ret; +} + +void ScreenshotEndpoint::handleScreenshotData(const QByteArray &data) +{ + WatchDataReader reader(data); + int offset = 0; + + if (m_waitingForMore == 0) { + + ResponseCode responseCode = (ResponseCode)reader.read<quint8>(); + if (responseCode != ResponseCodeOK) { + qWarning() << "Error taking screenshot:" << responseCode; + return; + } + m_version = reader.read<quint32>(); + + m_width = reader.read<quint32>(); + m_height = reader.read<quint32>(); + + switch (m_version) { + case 1: + m_waitingForMore = m_width * m_height / 8; + break; + case 2: + m_waitingForMore = m_width * m_height; + break; + default: + qWarning() << "Unsupported screenshot format version"; + m_waitingForMore = m_width * m_height; // might work :) + } + + offset = 13; + m_accumulatedData.clear(); + } + + QByteArray tmp = reader.readBytes(data.length() - offset); + m_waitingForMore -= tmp.length(); + m_accumulatedData.append(tmp); + + if (m_waitingForMore == 0) { + QByteArray output; + switch (m_version) { + case 1: { + int rowBytes = m_width / 8; + for (quint32 row = 0; row < m_height; row++) { + for (quint32 col = 0; col < m_width; col++) { + char pixel = (m_accumulatedData.at(row * rowBytes + col / 8) >> (col % 8)) & 1; + output.append(pixel * 255); + output.append(pixel * 255); + output.append(pixel * 255); + } + } + break; + } + case 2: + for (quint32 row = 0; row < m_height; row++) { + for (quint32 col = 0; col < m_width; col++) { + char pixel = m_accumulatedData.at(row * m_width + col); + output.append(((pixel >> 4) & 0b11) * 85); + output.append(((pixel >> 2) & 0b11) * 85); + output.append(((pixel >> 0) & 0b11) * 85); + } + } + break; + default: + qWarning() << "Invalid format."; + return; + } + + QImage image = QImage((uchar*)output.data(), m_width, m_height, QImage::Format_RGB888); + QDir dir(m_pebble->storagePath() + "/screenshots/"); + if (!dir.exists()) { + dir.mkpath(dir.absolutePath()); + } + QString filename = dir.absolutePath() + "/" + QDateTime::currentDateTime().toString("yyyyMMddHHmmss") + ".jpg"; + image.save(filename); + qDebug() << "Screenshot saved to" << filename; + emit screenshotAdded(filename); + } +} + + +QByteArray ScreenshotRequestPackage::serialize() const +{ + QByteArray data; + WatchDataWriter writer(&data); + + writer.write<quint8>(m_command); + return data; +} diff --git a/rockworkd/libpebble/screenshotendpoint.h b/rockworkd/libpebble/screenshotendpoint.h new file mode 100644 index 0000000..cca6cfd --- /dev/null +++ b/rockworkd/libpebble/screenshotendpoint.h @@ -0,0 +1,52 @@ +#ifndef SCREENSHOTENDPOINT_H +#define SCREENSHOTENDPOINT_H + +#include <QObject> + +#include "watchconnection.h" +class Pebble; + +class ScreenshotRequestPackage: public PebblePacket +{ +public: + QByteArray serialize() const override; +private: + quint8 m_command = 0x00; +}; + +class ScreenshotEndpoint : public QObject +{ + Q_OBJECT +public: + enum ResponseCode { + ResponseCodeOK = 0, + ResponseCodeMalformedCommand = 1, + ResponseCodeOutOfMemory = 2, + ResponseCodeAlreadyInProgress = 3 + }; + + explicit ScreenshotEndpoint(Pebble *pebble, WatchConnection *connection, QObject *parent = 0); + + void requestScreenshot(); + void removeScreenshot(const QString &filename); + + QStringList screenshots() const; + +signals: + void screenshotAdded(const QString &filename); + void screenshotRemoved(const QString &filename); + +private slots: + void handleScreenshotData(const QByteArray &data); + +private: + Pebble *m_pebble; + WatchConnection *m_connection; + quint32 m_waitingForMore = 0; + quint32 m_version = 0; + quint32 m_width = 0; + quint32 m_height = 0; + QByteArray m_accumulatedData; +}; + +#endif // SCREENSHOTENDPOINT_H diff --git a/rockworkd/libpebble/timelineitem.cpp b/rockworkd/libpebble/timelineitem.cpp new file mode 100644 index 0000000..4bc699c --- /dev/null +++ b/rockworkd/libpebble/timelineitem.cpp @@ -0,0 +1,144 @@ +#include "timelineitem.h" + +TimelineItem::TimelineItem(TimelineItem::Type type, Flags flags, const QDateTime ×tamp, quint16 duration): + TimelineItem(QUuid::createUuid(), type, flags, timestamp, duration) +{ + +} + +TimelineItem::TimelineItem(const QUuid &uuid, TimelineItem::Type type, Flags flags, const QDateTime ×tamp, quint16 duration): + PebblePacket(), + m_itemId(uuid), + m_timestamp(timestamp), + m_duration(duration), + m_type(type), + m_flags(flags) +{ + +} + +QUuid TimelineItem::itemId() const +{ + return m_itemId; +} + +void TimelineItem::setLayout(quint8 layout) +{ + m_layout = layout; +} + +void TimelineItem::setFlags(Flags flags) +{ + m_flags = flags; +} + +void TimelineItem::appendAttribute(const TimelineAttribute &attribute) +{ + m_attributes.append(attribute); +} + +void TimelineItem::appendAction(const TimelineAction &action) +{ + m_actions.append(action); +} + +QList<TimelineAttribute> TimelineItem::attributes() const +{ + return m_attributes; +} + +QList<TimelineAction> TimelineItem::actions() const +{ + return m_actions; +} + +QByteArray TimelineItem::serialize() const +{ + QByteArray ret; + ret.append(m_itemId.toRfc4122()); + ret.append(m_parentId.toRfc4122()); + int ts = m_timestamp.toMSecsSinceEpoch() / 1000; + ret.append(ts & 0xFF); ret.append((ts >> 8) & 0xFF); ret.append((ts >> 16) & 0xFF); ret.append((ts >> 24) & 0xFF); + ret.append(m_duration & 0xFF); ret.append(((m_duration >> 8) & 0xFF)); + ret.append((quint8)m_type); + ret.append(m_flags & 0xFF); ret.append(((m_flags >> 8) & 0xFF)); + ret.append(m_layout); + + QByteArray serializedAttributes; + foreach (const TimelineAttribute &attribute, m_attributes) { + serializedAttributes.append(attribute.serialize()); + } + + QByteArray serializedActions; + foreach (const TimelineAction &action, m_actions) { + serializedActions.append(action.serialize()); + } + quint16 dataLength = serializedAttributes.length() + serializedActions.length(); + ret.append(dataLength & 0xFF); ret.append(((dataLength >> 8) & 0xFF)); + ret.append(m_attributes.count()); + ret.append(m_actions.count()); + ret.append(serializedAttributes); + ret.append(serializedActions); + return ret; +} + +TimelineAction::TimelineAction(quint8 actionId, TimelineAction::Type type, const QList<TimelineAttribute> &attributes): + PebblePacket(), + m_actionId(actionId), + m_type(type), + m_attributes(attributes) +{ + +} + +void TimelineAction::appendAttribute(const TimelineAttribute &attribute) +{ + m_attributes.append(attribute); +} + +void TimelineAttribute::setContent(const QString &content) +{ + m_content = content.toUtf8(); +} + +void TimelineAttribute::setContent(TimelineAttribute::IconID iconId) +{ + m_content.clear(); + m_content.append((quint8)iconId); + m_content.append('\0'); + m_content.append('\0'); + m_content.append(0x80); +} + +void TimelineAttribute::setContent(TimelineAttribute::Color color) +{ + m_content.clear(); + m_content.append((quint8)color); +} + +void TimelineAttribute::setContent(const QStringList &values) +{ + m_content.clear(); + foreach (const QString &value, values) { + if (!m_content.isEmpty()) { + m_content.append('\0'); + } + m_content.append(value.toUtf8()); + } +} + +void TimelineAttribute::setContent(quint8 data) +{ + m_content.clear(); + m_content.append(data); +} + +QByteArray TimelineAttribute::serialize() const +{ + QByteArray ret; + ret.append((quint8)m_type); + ret.append(m_content.length() & 0xFF); ret.append(((m_content.length() >> 8) & 0xFF)); // length + ret.append(m_content); + return ret; +} + diff --git a/rockworkd/libpebble/timelineitem.h b/rockworkd/libpebble/timelineitem.h new file mode 100644 index 0000000..ed35539 --- /dev/null +++ b/rockworkd/libpebble/timelineitem.h @@ -0,0 +1,194 @@ +#ifndef TIMELINEITEM_H +#define TIMELINEITEM_H + +#include <QByteArray> +#include <QDateTime> + +#include "watchconnection.h" + + +class TimelineAttribute +{ +public: + enum Type { + TypeTitle = 0x01, + TypeSubtitle = 0x02, + TypeBody = 0x03, + TypeTinyIcon = 0x04, + TypeLargeIcon = 0x06, + TypeFieldNames = 0x19, + TypeFieldValues = 0x1a, + TypeColor = 0x1c, + TypeRecurring = 0x1f + }; + enum IconID { + IconIDDefaultBell = 0x01, + IconIDDefaultMissedCall = 0x02, + IconIDReminder = 0x03, + IconIDFlag = 0x04, + IconIDWhatsApp = 0x05, + IconIDTwitter = 0x06, + IconIDTelegram = 0x07, + IconIDHangout = 0x08, + IconIDGMail = 0x09, + IconIDFlash = 0x0a, // TODO: what service is this? + IconIDFacebook = 0x0b, + IconIDMusic = 0x0c, + IconIDAlarm = 0x0d, + IconIDWeather = 0x0e, + IconIDGuess = 0x31 + }; + + enum Color { + ColorWhite = 0x00, + ColorBlack = 0x80, + ColorDarkBlue = 0x81, + ColorBlue = 0x82, + ColorLightBlue = 0x83, + ColorDarkGreen = 0x84, + ColorGray = 0x85, + ColorBlue2 = 0x86, + ColorLightBlue2 = 0x87, + ColorGreen = 0x88, + ColorOliveGreen = 0x89, + ColorLightGreen = 0x90, + ColorViolet = 0x91, + ColorViolet2 = 0x91, + ColorBlue3 = 0x92, + ColorBrown = 0x93, + ColorGray2 = 0x94, + ColorBlue4 = 0x95, + ColorBlue5 = 0x96, + ColorRed = 0xA0, + ColorOrange = 0xB8, + ColorYellow = 0xBC + }; + + TimelineAttribute(Type type, const QByteArray &content): + m_type(type), + m_content(content) + {} + + TimelineAttribute(Type type, IconID iconId): + m_type(type) + { + setContent(iconId); + } + TimelineAttribute(Type type, Color color): + m_type(type) + { + setContent(color); + } + TimelineAttribute(Type type, const QStringList &values): + m_type(type) + { + setContent(values); + } + TimelineAttribute(Type type, quint8 data): + m_type(type) + { + setContent(data); + } + + void setContent(const QString &content); + void setContent(IconID iconId); + void setContent(Color color); + void setContent(const QStringList &values); + void setContent(quint8 data); + + QByteArray serialize() const; +private: + Type m_type; + QByteArray m_content; +}; + +class TimelineAction: public PebblePacket +{ +public: + enum Type { + TypeAncsDismiss = 1, + TypeGeneric = 2, + TypeResponse = 3, + TypeDismiss = 4, + TypeHTTP = 5, + TypeSnooze = 6, + TypeOpenWatchApp = 7, + TypeEmpty = 8, + TypeRemove = 9, + TypeOpenPin = 10 + }; + TimelineAction(quint8 actionId, Type type, const QList<TimelineAttribute> &attributes = QList<TimelineAttribute>()); + void appendAttribute(const TimelineAttribute &attribute); + + QByteArray serialize() const override { + QByteArray ret; + ret.append(m_actionId); + ret.append((quint8)m_type); + ret.append(m_attributes.count()); + foreach (const TimelineAttribute &attr, m_attributes) { + ret.append(attr.serialize()); + } + return ret; + } + +private: + quint8 m_actionId; + Type m_type; + QList<TimelineAttribute> m_attributes; +}; + +class TimelineItem: public PebblePacket +{ +public: + enum Type { + TypeNotification = 1, + TypePin = 2, + TypeReminder = 3 + }; + + // TODO: this is probably not complete and maybe even wrong. + enum Flag { + FlagNone = 0x00, + FlagSingleEvent = 0x01, + FlagTimeInUTC = 0x02, + FlagAllDay = 0x04 + }; + Q_DECLARE_FLAGS(Flags, Flag) + + // TODO: This is not complete + enum Layout { + LayoutGenericPin = 0x01, + LayoutCalendar = 0x02 + }; + + TimelineItem(Type type, TimelineItem::Flags flags = FlagNone, const QDateTime ×tamp = QDateTime::currentDateTime(), quint16 duration = 0); + TimelineItem(const QUuid &uuid, Type type, Flags flags = FlagNone, const QDateTime ×tamp = QDateTime::currentDateTime(), quint16 duration = 0); + + QUuid itemId() const; + + void setLayout(quint8 layout); + void setFlags(Flags flags); + + void appendAttribute(const TimelineAttribute &attribute); + void appendAction(const TimelineAction &action); + + QList<TimelineAttribute> attributes() const; + QList<TimelineAction> actions() const; + + QByteArray serialize() const override; + +private: + QUuid m_itemId; + QUuid m_parentId; + QDateTime m_timestamp; + quint16 m_duration = 0; + Type m_type; + Flags m_flags; // quint16 + quint8 m_layout = 0x01; // TODO: find out what this is about + QList<TimelineAttribute> m_attributes; + QList<TimelineAction> m_actions; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(TimelineItem::Flags) + +#endif // TIMELINEITEM_H diff --git a/rockworkd/libpebble/uploadmanager.cpp b/rockworkd/libpebble/uploadmanager.cpp new file mode 100644 index 0000000..6c6860f --- /dev/null +++ b/rockworkd/libpebble/uploadmanager.cpp @@ -0,0 +1,331 @@ +#include "uploadmanager.h" +#include "watchdatareader.h" +#include "watchdatawriter.h" + +static const int CHUNK_SIZE = 2000; + +UploadManager::UploadManager(WatchConnection *connection, QObject *parent) : + QObject(parent), m_connection(connection), + _lastUploadId(0), _state(StateNotStarted) +{ + m_connection->registerEndpointHandler(WatchConnection::EndpointPutBytes, this, "handlePutBytesMessage"); +} + +uint UploadManager::upload(WatchConnection::UploadType type, int index, quint32 appInstallId, const QString &filename, int size, quint32 crc, + SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + qDebug() << "Should enqueue uplodad:" << filename; + PendingUpload upload; + upload.id = ++_lastUploadId; + upload.type = type; + upload.index = index; + upload.filename = filename; + upload.appInstallId = appInstallId; + QFile *f = new QFile(filename); + if (!f->open(QFile::ReadOnly)) { + qWarning() << "Error opening file" << filename << "for reading. Cannot upload file"; + if (errorCallback) { + errorCallback(-1); + } + } + upload.device = f; + if (size < 0) { + upload.size = f->size(); + } else { + upload.size = size; + } + upload.remaining = upload.size; + upload.crc = crc; + upload.successCallback = successCallback; + upload.errorCallback = errorCallback; + upload.progressCallback = progressCallback; + + if (upload.remaining <= 0) { + qWarning() << "upload is empty"; + if (errorCallback) { + errorCallback(-1); + return -1; + } + } + + _pending.enqueue(upload); + + if (_pending.size() == 1) { + startNextUpload(); + } + + return upload.id; +} + +uint UploadManager::uploadAppBinary(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(WatchConnection::UploadTypeBinary, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadAppResources(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(WatchConnection::UploadTypeResources, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadFile(const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(WatchConnection::UploadTypeFile, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadFirmwareBinary(bool recovery, const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(recovery ? WatchConnection::UploadTypeRecovery: WatchConnection::UploadTypeFirmware, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadFirmwareResources(const QString &filename, quint32 crc, SuccessCallback successCallback, ErrorCallback errorCallback, ProgressCallback progressCallback) +{ + return upload(WatchConnection::UploadTypeSystemResources, 0, 0, filename, -1, crc, successCallback, errorCallback, progressCallback); +} + +uint UploadManager::uploadAppWorker(quint32 appInstallId, const QString &filename, quint32 crc, UploadManager::SuccessCallback successCallback, UploadManager::ErrorCallback errorCallback, UploadManager::ProgressCallback progressCallback) +{ + return upload(WatchConnection::UploadTypeWorker, -1, appInstallId, filename, -1, crc, successCallback, errorCallback, progressCallback); +} + +void UploadManager::cancel(uint id, int code) +{ + if (_pending.empty()) { + qWarning() << "cannot cancel, empty queue"; + return; + } + + if (id == _pending.head().id) { + PendingUpload upload = _pending.dequeue(); + qDebug() << "aborting current upload" << id << "(code:" << code << ")"; + + if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) { + QByteArray msg; + WatchDataWriter writer(&msg); + writer.write<quint8>(PutBytesCommandAbort); + writer.write<quint32>(_token); + + qDebug() << "sending abort for upload" << id; + + m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg); + } + + _state = StateNotStarted; + _token = 0; + + if (upload.errorCallback) { + upload.errorCallback(code); + } + upload.device->deleteLater(); + + if (!_pending.empty()) { + startNextUpload(); + } + } else { + for (int i = 1; i < _pending.size(); ++i) { + if (_pending[i].id == id) { + qDebug() << "cancelling upload" << id << "(code:" << code << ")"; + if (_pending[i].errorCallback) { + _pending[i].errorCallback(code); + } + _pending.at(i).device->deleteLater(); + _pending.removeAt(i); + return; + } + } + qWarning() << "cannot cancel, id" << id << "not found"; + } +} + +void UploadManager::startNextUpload() +{ + Q_ASSERT(!_pending.empty()); + Q_ASSERT(_state == StateNotStarted); + + PendingUpload &upload = _pending.head(); + QByteArray msg; + WatchDataWriter writer(&msg); + writer.write<quint8>(PutBytesCommandInit); + writer.write<quint32>(upload.remaining); + if (upload.index != -1) { + writer.write<quint8>(upload.type); + writer.write<quint8>(upload.index); + if (!upload.filename.isEmpty()) { + writer.writeCString(upload.filename); + } + } else { + writer.write<quint8>(upload.type|0x80); + writer.writeLE<quint32>(upload.appInstallId); + } + + qDebug().nospace() << "starting new upload " << upload.id + << ", size:" << upload.remaining + << ", type:" << upload.type + << ", slot:" << upload.index + << ", crc:" << upload.crc + << ", filename:" << upload.filename; + + qDebug() << msg.toHex(); + + _state = StateWaitForToken; + m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg); +} + +bool UploadManager::uploadNextChunk(PendingUpload &upload) +{ + QByteArray chunk = upload.device->read(qMin<int>(upload.remaining, CHUNK_SIZE)); + + if (upload.remaining < CHUNK_SIZE && chunk.size() < upload.remaining) { + // Short read! + qWarning() << "short read during upload" << upload.id; + return false; + } + + Q_ASSERT(!chunk.isEmpty()); + Q_ASSERT(_state = StateInProgress); + + QByteArray msg; + WatchDataWriter writer(&msg); + writer.write<quint8>(PutBytesCommandSend); + writer.write<quint32>(_token); + writer.write<quint32>(chunk.size()); + msg.append(chunk); + + qDebug() << "sending a chunk of" << chunk.size() << "bytes"; + + m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg); + + upload.remaining -= chunk.size(); + + qDebug() << "remaining" << upload.remaining << "/" << upload.size << "bytes"; + + return true; +} + +bool UploadManager::commit(PendingUpload &upload) +{ + Q_ASSERT(_state == StateCommit); + Q_ASSERT(upload.remaining == 0); + + QByteArray msg; + WatchDataWriter writer(&msg); + writer.write<quint8>(PutBytesCommandCommit); + writer.write<quint32>(_token); + writer.write<quint32>(upload.crc); + + qDebug() << "commiting upload" << upload.id; + + m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg); + + return true; +} + +bool UploadManager::complete(PendingUpload &upload) +{ + Q_ASSERT(_state == StateComplete); + + QByteArray msg; + WatchDataWriter writer(&msg); + writer.write<quint8>(PutBytesCommandComplete); + writer.write<quint32>(_token); + + qDebug() << "completing upload" << upload.id; + + m_connection->writeToPebble(WatchConnection::EndpointPutBytes, msg); + + return true; +} + +void UploadManager::handlePutBytesMessage(const QByteArray &data) +{ + if (_pending.empty()) { + qWarning() << "putbytes message, but queue is empty!"; + return; + } + Q_ASSERT(!_pending.empty()); + PendingUpload &upload = _pending.head(); + + WatchDataReader reader(data); + int status = reader.read<quint8>(); + + if (reader.bad() || status != 1) { + qWarning() << "upload" << upload.id << "got error code=" << status; + cancel(upload.id, status); + return; + } + + quint32 recv_token = reader.read<quint32>(); + + if (reader.bad()) { + qWarning() << "upload" << upload.id << ": could not read the token"; + cancel(upload.id, -1); + return; + } + + if (_state != StateNotStarted && _state != StateWaitForToken && _state != StateComplete) { + if (recv_token != _token) { + qWarning() << "upload" << upload.id << ": invalid token"; + cancel(upload.id, -1); + return; + } + } + + switch (_state) { + case StateNotStarted: + qWarning() << "got packet when upload is not started"; + break; + case StateWaitForToken: + qDebug() << "token received"; + _token = recv_token; + _state = StateInProgress; + + /* fallthrough */ + case StateInProgress: + qDebug() << "moving to the next chunk"; + if (upload.progressCallback) { + // Report that the previous chunk has been succesfully uploaded + upload.progressCallback(1.0 - (qreal(upload.remaining) / upload.size)); + } + if (upload.remaining > 0) { + if (!uploadNextChunk(upload)) { + cancel(upload.id, -1); + return; + } + } else { + qDebug() << "no additional chunks, commit"; + _state = StateCommit; + if (!commit(upload)) { + cancel(upload.id, -1); + return; + } + } + break; + case StateCommit: + qDebug() << "commited succesfully"; + if (upload.progressCallback) { + // Report that all chunks have been succesfully uploaded + upload.progressCallback(1.0); + } + _state = StateComplete; + if (!complete(upload)) { + cancel(upload.id, -1); + return; + } + break; + case StateComplete: + qDebug() << "upload" << upload.id << "succesful, invoking callback"; + if (upload.successCallback) { + upload.successCallback(); + } + upload.device->deleteLater(); + _pending.dequeue(); + _token = 0; + _state = StateNotStarted; + if (!_pending.empty()) { + startNextUpload(); + } + break; + default: + qWarning() << "received message in wrong state"; + break; + } +} diff --git a/rockworkd/libpebble/uploadmanager.h b/rockworkd/libpebble/uploadmanager.h new file mode 100644 index 0000000..a717417 --- /dev/null +++ b/rockworkd/libpebble/uploadmanager.h @@ -0,0 +1,85 @@ +#ifndef UPLOADMANAGER_H +#define UPLOADMANAGER_H + +#include <functional> +#include <QQueue> +#include "watchconnection.h" + +class UploadManager : public QObject +{ + Q_OBJECT + +public: + explicit UploadManager(WatchConnection *watch, QObject *parent = 0); + + typedef std::function<void()> SuccessCallback; + typedef std::function<void(int)> ErrorCallback; + typedef std::function<void(qreal)> ProgressCallback; + + uint upload(WatchConnection::UploadType type, int index, quint32 appInstallId, const QString &filename, int size = -1, quint32 crc = 0, + SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + + uint uploadAppBinary(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + uint uploadAppResources(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + uint uploadAppWorker(quint32 appInstallId, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + + uint uploadFirmwareBinary(bool recovery, const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + uint uploadFirmwareResources(const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + + uint uploadFile(const QString &filename, quint32 crc, SuccessCallback successCallback = SuccessCallback(), ErrorCallback errorCallback = ErrorCallback(), ProgressCallback progressCallback = ProgressCallback()); + + void cancel(uint id, int code = 0); + +signals: + +private: + enum State { + StateNotStarted, + StateWaitForToken, + StateInProgress, + StateCommit, + StateComplete + }; + + struct PendingUpload { + uint id; + + WatchConnection::UploadType type; + int index = -1; + QString filename; + quint32 appInstallId; + QIODevice *device; + int size; + int remaining; + quint32 crc; + + SuccessCallback successCallback; + ErrorCallback errorCallback; + ProgressCallback progressCallback; + }; + + enum PutBytesCommand { + PutBytesCommandInit = 1, + PutBytesCommandSend = 2, + PutBytesCommandCommit = 3, + PutBytesCommandAbort = 4, + PutBytesCommandComplete = 5 + }; + + void startNextUpload(); + bool uploadNextChunk(PendingUpload &upload); + bool commit(PendingUpload &upload); + bool complete(PendingUpload &upload); + +private slots: + void handlePutBytesMessage(const QByteArray &msg); + +private: + WatchConnection *m_connection; + QQueue<PendingUpload> _pending; + uint _lastUploadId; + State _state; + quint32 _token; +}; + +#endif // UPLOADMANAGER_H diff --git a/rockworkd/libpebble/watchconnection.cpp b/rockworkd/libpebble/watchconnection.cpp new file mode 100644 index 0000000..0778a1d --- /dev/null +++ b/rockworkd/libpebble/watchconnection.cpp @@ -0,0 +1,242 @@ +#include "watchconnection.h" +#include "watchdatareader.h" +#include "watchdatawriter.h" +#include "uploadmanager.h" + +#include <QDBusConnection> +#include <QDBusReply> +#include <QDebug> +#include <QBluetoothAddress> +#include <QBluetoothLocalDevice> +#include <QBluetoothSocket> +#include <QtEndian> +#include <QDateTime> + +WatchConnection::WatchConnection(QObject *parent) : + QObject(parent), + m_socket(nullptr) +{ + m_reconnectTimer.setSingleShot(true); + QObject::connect(&m_reconnectTimer, &QTimer::timeout, this, &WatchConnection::reconnect); + + m_localDevice = new QBluetoothLocalDevice(this); + connect(m_localDevice, &QBluetoothLocalDevice::hostModeStateChanged, this, &WatchConnection::hostModeStateChanged); + + m_uploadManager = new UploadManager(this, this); +} + +UploadManager *WatchConnection::uploadManager() const +{ + return m_uploadManager; +} + +void WatchConnection::scheduleReconnect() +{ + if (m_connectionAttempts == 0) { + reconnect(); + } else if (m_connectionAttempts < 25) { + qDebug() << "Attempting to reconnect in 10 seconds"; + m_reconnectTimer.start(1000 * 10); + } else if (m_connectionAttempts < 35) { + qDebug() << "Attempting to reconnect in 1 minute"; + m_reconnectTimer.start(1000 * 60); + } else { + qDebug() << "Attempting to reconnect in 15 minutes"; + m_reconnectTimer.start(1000 * 60 * 15); + } +} + +void WatchConnection::reconnect() +{ + QBluetoothLocalDevice localBtDev; + if (localBtDev.pairingStatus(m_pebbleAddress) == QBluetoothLocalDevice::Unpaired) { + // Try again in one 10 secs, give the user some time to pair it + m_connectionAttempts = 1; + scheduleReconnect(); + return; + } + + if (m_socket) { + if (m_socket->state() == QBluetoothSocket::ConnectedState) { + qDebug() << "Already connected."; + return; + } + delete m_socket; + } + + m_socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this); + connect(m_socket, &QBluetoothSocket::connected, this, &WatchConnection::pebbleConnected); + connect(m_socket, &QBluetoothSocket::readyRead, this, &WatchConnection::readyRead); + connect(m_socket, SIGNAL(error(QBluetoothSocket::SocketError)), this, SLOT(socketError(QBluetoothSocket::SocketError))); + connect(m_socket, &QBluetoothSocket::disconnected, this, &WatchConnection::pebbleDisconnected); + //connect(socket, SIGNAL(bytesWritten(qint64)), SLOT(onBytesWritten(qint64))); + + m_connectionAttempts++; + + // FIXME: Assuming port 1 (with Pebble) + m_socket->connectToService(m_pebbleAddress, 1); +} + +void WatchConnection::connectPebble(const QBluetoothAddress &pebble) +{ + m_pebbleAddress = pebble; + m_connectionAttempts = 0; + scheduleReconnect(); +} + +bool WatchConnection::isConnected() +{ + return m_socket && m_socket->state() == QBluetoothSocket::ConnectedState; +} + +void WatchConnection::writeToPebble(Endpoint endpoint, const QByteArray &data) +{ + if (!m_socket || m_socket->state() != QBluetoothSocket::ConnectedState) { + qWarning() << "Socket not open. Cannot send data to Pebble. (Endpoint:" << endpoint << ")"; + return; + } + + //qDebug() << "sending message to endpoint" << endpoint; + QByteArray msg; + + msg.append((data.length() & 0xFF00) >> 8); + msg.append(data.length() & 0xFF); + + msg.append((endpoint & 0xFF00) >> 8); + msg.append(endpoint & 0xFF); + + msg.append(data); + + //qDebug() << "Writing:" << msg.toHex(); + m_socket->write(msg); +} + +void WatchConnection::systemMessage(WatchConnection::SystemMessage msg) +{ + QByteArray data; + data.append((char)0); + data.append((char)msg); + writeToPebble(EndpointSystemMessage, data); +} + +bool WatchConnection::registerEndpointHandler(WatchConnection::Endpoint endpoint, QObject *handler, const QString &method) +{ + if (m_endpointHandlers.contains(endpoint)) { + qWarning() << "Already have a handlder for endpoint" << endpoint; + return false; + } + Callback cb; + cb.obj = handler; + cb.method = method; + m_endpointHandlers.insert(endpoint, cb); + return true; +} + +void WatchConnection::pebbleConnected() +{ + m_connectionAttempts = 0; + emit watchConnected(); +} + +void WatchConnection::pebbleDisconnected() +{ + qDebug() << "Disconnected"; + m_socket->close(); + emit watchDisconnected(); + if (!m_reconnectTimer.isActive()) { + scheduleReconnect(); + } +} + +void WatchConnection::socketError(QBluetoothSocket::SocketError error) +{ + Q_UNUSED(error); // We seem to get UnknownError anyways all the time + qDebug() << "SocketError" << error; + m_socket->close(); + emit watchConnectionFailed(); + if (!m_reconnectTimer.isActive()) { + scheduleReconnect(); + } +} + +void WatchConnection::readyRead() +{ +// QByteArray data = m_socket->readAll(); +// qDebug() << "data from pebble" << data.toHex(); + +// QByteArray header = data.left(4); +// qDebug() << "header:" << header.toHex(); + if (!m_socket) { + return; + } + int headerLength = 4; + uchar header[4]; + m_socket->peek(reinterpret_cast<char*>(header), headerLength); + + quint16 messageLength = qFromBigEndian<quint16>(&header[0]); + Endpoint endpoint = (Endpoint)qFromBigEndian<quint16>(&header[2]); + + if (m_socket->bytesAvailable() < headerLength + messageLength) { +// qDebug() << "not enough data... waiting for more"; + return; + } + + QByteArray data = m_socket->read(headerLength + messageLength); +// qDebug() << "Have message for endpoint:" << endpoint << "data:" << data.toHex(); + + data = data.right(data.length() - 4); + + if (m_endpointHandlers.contains(endpoint)) { + if (m_endpointHandlers.contains(endpoint)) { + Callback cb = m_endpointHandlers.value(endpoint); + QMetaObject::invokeMethod(cb.obj.data(), cb.method.toLatin1(), Q_ARG(QByteArray, data)); + } + } else { + qWarning() << "Have message for unhandled endpoint" << endpoint << data.toHex(); + } + + if (m_socket->bytesAvailable() > 0) { + readyRead(); + } +} + +void WatchConnection::hostModeStateChanged(QBluetoothLocalDevice::HostMode state) +{ + switch (state) { + case QBluetoothLocalDevice::HostPoweredOff: + qDebug() << "Bluetooth turned off. Stopping any reconnect attempts."; + m_reconnectTimer.stop(); + break; + case QBluetoothLocalDevice::HostConnectable: + case QBluetoothLocalDevice::HostDiscoverable: + case QBluetoothLocalDevice::HostDiscoverableLimitedInquiry: + if (m_socket && m_socket->state() != QBluetoothSocket::ConnectedState + && m_socket->state() != QBluetoothSocket::ConnectingState + && !m_reconnectTimer.isActive()) { + qDebug() << "Bluetooth now active. Trying to reconnect"; + m_connectionAttempts = 0; + scheduleReconnect(); + } + } +} + +QByteArray WatchConnection::buildData(QStringList data) +{ + QByteArray res; + for (QString d : data) + { + QByteArray tmp = d.left(0xEF).toUtf8(); + res.append((tmp.length() + 1) & 0xFF); + res.append(tmp); + res.append('\0'); + } + return res; +} + +QByteArray WatchConnection::buildMessageData(uint lead, QStringList data) +{ + QByteArray res; + res.append(lead & 0xFF); + res.append(buildData(data)); + return res; +} diff --git a/rockworkd/libpebble/watchconnection.h b/rockworkd/libpebble/watchconnection.h new file mode 100644 index 0000000..f2c3d5f --- /dev/null +++ b/rockworkd/libpebble/watchconnection.h @@ -0,0 +1,154 @@ +#ifndef WATCHCONNECTION_H +#define WATCHCONNECTION_H + +#include <QObject> +#include <QBluetoothAddress> +#include <QBluetoothSocket> +#include <QBluetoothLocalDevice> +#include <QtEndian> +#include <QPointer> +#include <QTimer> +#include <QFile> + +class EndpointHandlerInterface; +class UploadManager; + +class PebblePacket { +public: + PebblePacket() {} + virtual ~PebblePacket() = default; + virtual QByteArray serialize() const = 0; + QByteArray packString(const QString &string) const { + QByteArray tmp = string.left(0xEF).toUtf8(); + QByteArray ret; + ret.append((tmp.length() + 1) & 0xFF); + ret.append(tmp); + ret.append('\0'); + return ret; + } +}; + +class Callback +{ +public: + QPointer<QObject> obj; + QString method; +}; + +class WatchConnection : public QObject +{ + Q_OBJECT +public: + + enum Endpoint { + EndpointUnknownEndpoint = 0, + EndpointTime = 11, + EndpointVersion = 16, + EndpointPhoneVersion = 17, + EndpointSystemMessage = 18, + EndpointMusicControl = 32, + EndpointPhoneControl = 33, + EndpointApplicationMessage = 48, + EndpointLauncher = 49, + EndpointAppLaunch = 52, + EndpointWatchLogs = 2000, +// EndpointWatchPing = 2001, + EndpointLogDump = 2002, +// EndpointWatchReset = 2003, +// EndpointWatchApp = 2004, +// EndpointAppLogs = 2006, + EndpointNotification = 3000, +// watchEXTENSIBLE_NOTIFS = 3010, // Deprecated in 3.x +// watchRESOURCE = 4000, + EndpointFactorySettings = 5001, + EndpointAppManager = 6000, // Deprecated in 3.x + EndpointAppFetch = 6001, // New in 3.x + EndpointDataLogging = 6778, + EndpointScreenshot = 8000, +// watchFILE_MANAGER = 8181, +// watchCORE_DUMP = 9000, +// watchAUDIO = 10000, // New in 3.x + EndpointActionHandler = 11440, + EndpointBlobDB = 45531, // New in 3.x + EndpointSorting = 0xabcd, + EndpointPutBytes = 0xbeef + }; + + enum SystemMessage { + SystemMessageFirmwareAvailable = 0, + SystemMessageFirmwareStart = 1, + SystemMessageFirmwareComplete = 2, + SystemMessageFirmwareFail = 3, + SystemMessageFirmwareUpToDate = 4, + SystemMessageFirmwareOutOfDate = 5, + SystemMessageBluetoothStartDiscoverable = 6, + SystemMessageBluetoothEndDiscoverable = 7 + }; + + typedef QMap<int, QVariant> Dict; + enum DictItemType { + DictItemTypeBytes, + DictItemTypeString, + DictItemTypeUInt, + DictItemTypeInt + }; + + enum UploadType { + UploadTypeFirmware = 1, + UploadTypeRecovery = 2, + UploadTypeSystemResources = 3, + UploadTypeResources = 4, + UploadTypeBinary = 5, + UploadTypeFile = 6, + UploadTypeWorker = 7 + }; + enum UploadStatus { + UploadStatusProgress, + UploadStatusFailed, + UploadStatusSuccess + }; + + explicit WatchConnection(QObject *parent = 0); + UploadManager *uploadManager() const; + + void connectPebble(const QBluetoothAddress &pebble); + bool isConnected(); + + QByteArray buildData(QStringList data); + QByteArray buildMessageData(uint lead, QStringList data); + + void writeToPebble(Endpoint endpoint, const QByteArray &data); + void systemMessage(SystemMessage msg); + + bool registerEndpointHandler(Endpoint endpoint, QObject *handler, const QString &method); + +signals: + void watchConnected(); + void watchDisconnected(); + void watchConnectionFailed(); + +private: + void scheduleReconnect(); + void reconnect(); + +private slots: + void hostModeStateChanged(QBluetoothLocalDevice::HostMode state); + void pebbleConnected(); + void pebbleDisconnected(); + void socketError(QBluetoothSocket::SocketError error); + void readyRead(); +// void logData(const QByteArray &data); + + +private: + QBluetoothAddress m_pebbleAddress; + QBluetoothLocalDevice *m_localDevice; + QBluetoothSocket *m_socket = nullptr; + int m_connectionAttempts = 0; + QTimer m_reconnectTimer; + + UploadManager *m_uploadManager; + QHash<Endpoint, Callback> m_endpointHandlers; +}; + +#endif // WATCHCONNECTION_H diff --git a/rockworkd/libpebble/watchdatareader.cpp b/rockworkd/libpebble/watchdatareader.cpp new file mode 100644 index 0000000..0c73c73 --- /dev/null +++ b/rockworkd/libpebble/watchdatareader.cpp @@ -0,0 +1,6 @@ +#include "watchdatareader.h" + +bool WatchDataReader::bad() const +{ + return m_bad; +} diff --git a/rockworkd/libpebble/watchdatareader.h b/rockworkd/libpebble/watchdatareader.h new file mode 100644 index 0000000..58e77d8 --- /dev/null +++ b/rockworkd/libpebble/watchdatareader.h @@ -0,0 +1,146 @@ +#ifndef WATCHDATAREADER_H +#define WATCHDATAREADER_H + +#include "watchconnection.h" + +#include <QByteArray> +#include <QtEndian> +#include <QString> +#include <QUuid> +#include <QMap> + +class WatchDataReader { +public: + WatchDataReader(const QByteArray &data): + m_data(data) + { + } + + template <typename T> + T read() { + if (checkBad(sizeof(T))) return 0; + const uchar *u = p(); + m_offset += sizeof(T); + return qFromBigEndian<T>(u); + } + + inline bool checkBad(int n = 0) + { + if (m_offset + n > m_data.size()) { + m_bad = true; + } + return m_bad; + } + inline const uchar * p() + { + return reinterpret_cast<const uchar *>(&m_data.constData()[m_offset]); + } + inline void skip(int n) + { + m_offset += n; + checkBad(); + } + + template <typename T> + inline T readLE() + { + if (checkBad(sizeof(T))) return 0; + const uchar *u = p(); + m_offset += sizeof(T); + return qFromLittleEndian<T>(u); + } + QString readFixedString(int n) + { + if (checkBad(n)) return QString(); + const char *u = &m_data.constData()[m_offset]; + m_offset += n; + return QString::fromUtf8(u, strnlen(u, n)); + } + QByteArray peek(int n) { + return m_data.left(m_offset + n).right(n); + } + QUuid readUuid() + { + if (checkBad(16)) return QString(); + m_offset += 16; + return QUuid::fromRfc4122(m_data.mid(m_offset - 16, 16)); + } + QByteArray readBytes(int n) + { + if (checkBad(n)) return QByteArray(); + const char *u = &m_data.constData()[m_offset]; + m_offset += n; + return QByteArray(u, n); + } + QMap<int, QVariant> readDict() + { + QMap<int, QVariant> d; + if (checkBad(1)) return d; + + const int n = readLE<quint8>(); + + for (int i = 0; i < n; i++) { + if (checkBad(4 + 1 + 2)) return d; + const int key = readLE<qint32>(); // For some reason, this is little endian. + const int type = readLE<quint8>(); + const int width = readLE<quint16>(); + + switch (type) { + case WatchConnection::DictItemTypeBytes: + d.insert(key, QVariant::fromValue(readBytes(width))); + break; + case WatchConnection::DictItemTypeString: + d.insert(key, QVariant::fromValue(readFixedString(width))); + break; + case WatchConnection::DictItemTypeUInt: + switch (width) { + case sizeof(quint8): + d.insert(key, QVariant::fromValue(readLE<quint8>())); + break; + case sizeof(quint16): + d.insert(key, QVariant::fromValue(readLE<quint16>())); + break; + case sizeof(quint32): + d.insert(key, QVariant::fromValue(readLE<quint32>())); + break; + default: + m_bad = true; + return d; + } + + break; + case WatchConnection::DictItemTypeInt: + switch (width) { + case sizeof(qint8): + d.insert(key, QVariant::fromValue(readLE<qint8>())); + break; + case sizeof(qint16): + d.insert(key, QVariant::fromValue(readLE<qint16>())); + break; + case sizeof(qint32): + d.insert(key, QVariant::fromValue(readLE<qint32>())); + break; + default: + m_bad = true; + return d; + } + + break; + default: + m_bad = true; + return d; + } + } + + return d; + } + bool bad() const; + + +private: + QByteArray m_data; + int m_offset = 0; + bool m_bad = false; +}; + +#endif // WATCHDATAREADER_H diff --git a/rockworkd/libpebble/watchdatawriter.cpp b/rockworkd/libpebble/watchdatawriter.cpp new file mode 100644 index 0000000..e3caf17 --- /dev/null +++ b/rockworkd/libpebble/watchdatawriter.cpp @@ -0,0 +1,144 @@ +#include "watchdatawriter.h" +#include "watchconnection.h" + +void WatchDataWriter::writeBytes(int n, const QByteArray &b) +{ + if (b.size() > n) { + _buf->append(b.constData(), n); + } else { + int diff = n - b.size(); + _buf->append(b); + if (diff > 0) { + _buf->append(QByteArray(diff, '\0')); + } + } +} + +void WatchDataWriter::writeFixedString(int n, const QString &s) +{ + _buf->append(s.left(n).toUtf8()); + for (int i = s.left(n).length(); i < n; i++) { + _buf->append('\0'); + } +} + +void WatchDataWriter::writeCString(const QString &s) +{ + _buf->append(s.toUtf8()); + _buf->append('\0'); +} + +void WatchDataWriter::writePascalString(const QString &s) +{ + _buf->append(s.length()); + _buf->append(s.toLatin1()); +} + +void WatchDataWriter::writeUuid(const QUuid &uuid) +{ + writeBytes(16, uuid.toRfc4122()); +} + +void WatchDataWriter::writeDict(const QMap<int, QVariant> &d) +{ + int size = d.size(); + if (size > 0xFF) { + qWarning() << "Dictionary is too large to encode"; + writeLE<quint8>(0); + return; + } + + writeLE<quint8>(size); + + for (QMap<int, QVariant>::const_iterator it = d.constBegin(); it != d.constEnd(); ++it) { + writeLE<quint32>(it.key()); + + switch (int(it.value().type())) { + case QMetaType::Char: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(char)); + writeLE<char>(it.value().value<char>()); + break; + case QMetaType::Short: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(short)); + writeLE<short>(it.value().value<short>()); + break; + case QMetaType::Int: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(int)); + writeLE<int>(it.value().value<int>()); + break; + + case QMetaType::UChar: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(char)); + writeLE<char>(it.value().value<char>()); + break; + case QMetaType::UShort: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(short)); + writeLE<short>(it.value().value<short>()); + break; + case QMetaType::UInt: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(int)); + writeLE<int>(it.value().value<int>()); + break; + + case QMetaType::Bool: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(char)); + writeLE<char>(it.value().value<char>()); + break; + + case QMetaType::Float: // Treat qreals as ints + case QMetaType::Double: + writeLE<quint8>(WatchConnection::DictItemTypeInt); + writeLE<quint16>(sizeof(int)); + writeLE<int>(it.value().value<int>()); + break; + + case QMetaType::QByteArray: { + QByteArray ba = it.value().toByteArray(); + writeLE<quint8>(WatchConnection::DictItemTypeBytes); + writeLE<quint16>(ba.size()); + _buf->append(ba); + break; + } + + case QMetaType::QVariantList: { + // Generally a JS array, which we marshal as a byte array. + QVariantList list = it.value().toList(); + QByteArray ba; + ba.reserve(list.size()); + + Q_FOREACH (const QVariant &v, list) { + ba.append(v.toInt()); + } + + writeLE<quint8>(WatchConnection::DictItemTypeBytes); + writeLE<quint16>(ba.size()); + _buf->append(ba); + break; + } + + default: + qWarning() << "Unknown dict item type:" << it.value().typeName(); + /* Fallthrough */ + case QMetaType::QString: + case QMetaType::QUrl: + { + QByteArray s = it.value().toString().toUtf8(); + if (s.isEmpty() || s[s.size() - 1] != '\0') { + // Add null terminator if it doesn't have one + s.append('\0'); + } + writeLE<quint8>(WatchConnection::DictItemTypeString); + writeLE<quint16>(s.size()); + _buf->append(s); + break; + } + } + } +} diff --git a/rockworkd/libpebble/watchdatawriter.h b/rockworkd/libpebble/watchdatawriter.h new file mode 100644 index 0000000..8e4adde --- /dev/null +++ b/rockworkd/libpebble/watchdatawriter.h @@ -0,0 +1,69 @@ +#ifndef WATCHDATAWRITER_H +#define WATCHDATAWRITER_H + +#include <QtEndian> +#include <QByteArray> +#include <QString> +#include <QUuid> +#include <QVariantMap> +#include <QLoggingCategory> + +class WatchDataWriter +{ +public: + WatchDataWriter(QByteArray *buf); + + template <typename T> + void write(T v); + + template <typename T> + void writeLE(T v); + + void writeBytes(int n, const QByteArray &b); + + void writeFixedString(int n, const QString &s); + + void writeCString(const QString &s); + + void writePascalString(const QString &s); + + void writeUuid(const QUuid &uuid); + + void writeDict(const QMap<int, QVariant> &d); + +private: + char *p(int n); + uchar *up(int n); + QByteArray *_buf; +}; + +inline WatchDataWriter::WatchDataWriter(QByteArray *buf) + : _buf(buf) +{ +} + +template <typename T> +void WatchDataWriter::write(T v) +{ + qToBigEndian(v, up(sizeof(T))); +} + +template <typename T> +void WatchDataWriter::writeLE(T v) +{ + qToLittleEndian(v, up(sizeof(T))); +} + +inline char * WatchDataWriter::p(int n) +{ + int size = _buf->size(); + _buf->resize(size + n); + return &_buf->data()[size]; +} + +inline uchar * WatchDataWriter::up(int n) +{ + return reinterpret_cast<uchar *>(p(n)); +} + +#endif diff --git a/rockworkd/libpebble/watchlogendpoint.cpp b/rockworkd/libpebble/watchlogendpoint.cpp new file mode 100644 index 0000000..4b6ab26 --- /dev/null +++ b/rockworkd/libpebble/watchlogendpoint.cpp @@ -0,0 +1,128 @@ +#include "watchlogendpoint.h" +#include "watchdatawriter.h" +#include "watchdatareader.h" +#include "pebble.h" +#include "ziphelper.h" + +#include <QDir> + +WatchLogEndpoint::WatchLogEndpoint(Pebble *pebble, WatchConnection *connection): + QObject(pebble), + m_pebble(pebble), + m_connection(connection) +{ + qsrand(QDateTime::currentMSecsSinceEpoch()); + m_connection->registerEndpointHandler(WatchConnection::EndpointLogDump, this, "logMessageReceived"); +} + +void WatchLogEndpoint::fetchLogs(const QString &fileName) +{ + if (m_currentEpoch != 0) { + qWarning() << "Already dumping logs. Not starting a second time"; + return; + } + + m_currentFile.setFileName(fileName); + if (!m_currentFile.open(QFile::WriteOnly | QFile::Truncate)) { + qWarning() << "Cannot open log file for writing" << m_currentFile.fileName(); + emit logsFetched(false); + return; + } + + fetchForEpoch(m_currentEpoch); +} + +void WatchLogEndpoint::fetchForEpoch(quint8 epoch) +{ + qDebug() << "Dumping logs for epoch" << epoch; + QString line("=== Generation: %1 ===\n"); + line = line.arg(epoch); + m_currentFile.write(line.toUtf8()); + RequestLogPacket packet(WatchLogEndpoint::LogCommandRequestLogs, epoch, qrand()); + m_connection->writeToPebble(WatchConnection::EndpointLogDump, packet.serialize()); +} + +void WatchLogEndpoint::logMessageReceived(const QByteArray &data) +{ + WatchDataReader reader(data); + quint8 command = reader.read<quint8>(); + switch (command) { + case LogCommandLogMessage: { + LogMessage m(data.right(data.length() - 1)); + QString line("%1 %2 :%3> %4\n"); + line = line.arg(m.level()).arg(m.timestamp().toString("yyyy-MM-dd hh:mm:ss")).arg(m.line()).arg(m.message()); + m_currentFile.write(line.toUtf8()); + break; + } + case LogCommandLogMessageDone: { + qDebug() << "Log for epoch" << m_currentEpoch << "fetched"; + m_currentEpoch++; + if (m_currentEpoch == 0) { + // Depending on the capabilities, there might not be a LogCommandNoLogMessages. Make sure we don't cycle endlessly + qDebug() << "All 255 epocs fetched. Stopping"; + m_currentFile.close(); + emit logsFetched(true); + return; + } + fetchForEpoch(m_currentEpoch); + break; + } + case LogCommandNoLogMessages: + qDebug() << "Log dumping finished"; + m_currentEpoch = 0; + m_currentFile.close(); + emit logsFetched(true); + break; + default: + qWarning() << "LogEndpoint: Unhandled command" << command; + } +} + +RequestLogPacket::RequestLogPacket(WatchLogEndpoint::LogCommand command, quint8 generation, quint32 cookie): + m_command(command), + m_generation(generation), + m_cookie(cookie) +{ + +} + +QByteArray RequestLogPacket::serialize() const +{ + QByteArray msg; + WatchDataWriter writer(&msg); + writer.write<quint8>(m_command); + writer.write<quint8>(m_generation); + writer.write<quint32>(m_cookie); + return msg; +} + +LogMessage::LogMessage(const QByteArray &data) +{ + WatchDataReader reader(data); + m_cookie = reader.read<quint32>(); + m_timestamp = QDateTime::fromTime_t(reader.read<quint32>()); + int level = reader.read<quint8>(); + switch (level) { + case 0: + m_level = '*'; + break; + case 1: + m_level = 'E'; + break; + case 50: + m_level = 'W'; + break; + case 100: + m_level = 'I'; + break; + case 200: + m_level = 'D'; + case 250: + m_level = 'V'; + } + + m_length = reader.read<quint8>(); + m_line = reader.read<quint16>(); + m_filename = reader.readFixedString(16); + m_message = reader.readFixedString(m_length); +} diff --git a/rockworkd/libpebble/watchlogendpoint.h b/rockworkd/libpebble/watchlogendpoint.h new file mode 100644 index 0000000..4ce58bf --- /dev/null +++ b/rockworkd/libpebble/watchlogendpoint.h @@ -0,0 +1,76 @@ +#ifndef WATCHLOGENDPOINT_H +#define WATCHLOGENDPOINT_H + +#include <QObject> +#include <QDateTime> + +#include "watchconnection.h" + +class Pebble; + +class LogMessage: public PebblePacket +{ +public: + LogMessage(const QByteArray &data); + + quint32 cookie() const { return m_cookie; } + QDateTime timestamp() const { return m_timestamp; } + QChar level() const { return m_level; } + quint8 length() const { return m_length; } + quint16 line() const { return m_line; } + QString filename() const { return m_filename; } + QString message() const { return m_message; } + + QByteArray serialize() const override { return QByteArray(); } +private: + quint32 m_cookie; + QDateTime m_timestamp; + QChar m_level; + quint8 m_length; + quint16 m_line; + QString m_filename; + QString m_message; +}; + +class WatchLogEndpoint : public QObject +{ + Q_OBJECT +public: + enum LogCommand { + LogCommandRequestLogs = 0x10, + LogCommandLogMessage = 0x80, + LogCommandLogMessageDone = 0x81, + LogCommandNoLogMessages = 0x82 + }; + + explicit WatchLogEndpoint(Pebble *pebble, WatchConnection *connection); + + void fetchLogs(const QString &fileName); + +signals: + void logsFetched(bool success); + +private slots: + void fetchForEpoch(quint8 epoch); + void logMessageReceived(const QByteArray &data); + +private: + Pebble *m_pebble; + WatchConnection *m_connection; + quint8 m_currentEpoch = 0; + QFile m_currentFile; + QString m_targetArchive; +}; + +class RequestLogPacket: public PebblePacket +{ +public: + RequestLogPacket(WatchLogEndpoint::LogCommand command, quint8 generation, quint32 cookie); + QByteArray serialize() const; +private: + WatchLogEndpoint::LogCommand m_command; + quint8 m_generation; + quint32 m_cookie; +}; + +#endif // WATCHLOGENDPOINT_H diff --git a/rockworkd/libpebble/ziphelper.cpp b/rockworkd/libpebble/ziphelper.cpp new file mode 100644 index 0000000..f18b8aa --- /dev/null +++ b/rockworkd/libpebble/ziphelper.cpp @@ -0,0 +1,91 @@ +#include "ziphelper.h" + +#include <QFileInfo> +#include <QDebug> +#include <QDir> + +#include <quazip/quazipfile.h> +#include <quazip/quazip.h> + +ZipHelper::ZipHelper() +{ + +} + +bool ZipHelper::unpackArchive(const QString &archiveFilename, const QString &targetDir) +{ + QuaZip zipFile(archiveFilename); + if (!zipFile.open(QuaZip::mdUnzip)) { + qWarning() << "Failed to open zip file" << zipFile.getZipName(); + return false; + } + + foreach (const QuaZipFileInfo &fi, zipFile.getFileInfoList()) { + QuaZipFile f(archiveFilename, fi.name); + if (!f.open(QFile::ReadOnly)) { + qWarning() << "could not extract file" << fi.name; + return false; + } + if (fi.name.endsWith("/")) { + qDebug() << "skipping" << fi.name; + continue; + } + qDebug() << "Inflating:" << fi.name; + QFileInfo dirInfo(targetDir + "/" + fi.name); + if (!dirInfo.absoluteDir().exists() && !dirInfo.absoluteDir().mkpath(dirInfo.absolutePath())) { + qWarning() << "Error creating target dir" << dirInfo.absoluteDir(); + return false; + } + QFile of(targetDir + "/" + fi.name); + if (!of.open(QFile::WriteOnly | QFile::Truncate)) { + qWarning() << "Could not open output file for writing" << fi.name; + f.close(); + return false; + } + of.write(f.readAll()); + f.close(); + of.close(); + } + return true; +} + +bool ZipHelper::packArchive(const QString &archiveFilename, const QString &sourceDir) +{ + QuaZip zip(archiveFilename); + if (!zip.open(QuaZip::mdCreate)){ + qWarning() << "Error creating zip file"; + return false; + } + + QDir dir(sourceDir); + QuaZipFile outfile(&zip); + + foreach (const QFileInfo &fi, dir.entryInfoList()) { + if (!fi.isFile()) { + continue; + } + qDebug() << "have file" << fi.absoluteFilePath(); + QuaZipNewInfo newInfo(fi.fileName(), fi.absoluteFilePath()); + + if (!outfile.open(QFile::WriteOnly, newInfo)) { + qWarning() << "Error opening zipfile for writing"; + zip.close(); + return false; + } + + QFile sourceFile(fi.absoluteFilePath()); + if (!sourceFile.open(QFile::ReadOnly)) { + qWarning() << "Error opening log file for reading" << fi.absoluteFilePath(); + outfile.close(); + zip.close(); + return false; + } + outfile.write(sourceFile.readAll()); + outfile.close(); + sourceFile.close(); + + } + outfile.close(); + zip.close(); + return true; +} diff --git a/rockworkd/libpebble/ziphelper.h b/rockworkd/libpebble/ziphelper.h new file mode 100644 index 0000000..fe3a7a1 --- /dev/null +++ b/rockworkd/libpebble/ziphelper.h @@ -0,0 +1,15 @@ +#ifndef ZIPHELPER_H +#define ZIPHELPER_H + +#include <QString> + +class ZipHelper +{ +public: + ZipHelper(); + + static bool unpackArchive(const QString &archiveFilename, const QString &targetDir); + static bool packArchive(const QString &archiveFilename, const QString &sourceDir); +}; + +#endif // ZIPHELPER_H 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\\\" |
