Merge branch 'master' into master

This commit is contained in:
Andreas Shimokawa 2017-08-27 00:44:00 +02:00 committed by GitHub
commit 1dbc22c235
163 changed files with 4677 additions and 1043 deletions

View File

@ -1,6 +1,7 @@
#### Before opening an issue please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
#### Your issue is:
*In case of a bug, do not forget to attach logs!*

View File

@ -1,11 +1,55 @@
###Changelog
### Changelog
###Version 0.19.1
#### Version 0.20.2 (next)
* Amazfit Bip: Various fixes regarding weather, add condition string support for FW 0.0.8.74
* Amazfit Bip: enable caller display in later firmwares
* Amazfit Bip: initial firmware update support (EXPERIMENTAL, AT YOUR OWN RISK)
#### Version 0.20.1
* Amazfit Bip: Support icons and text body for notifications
* Mi Band: Fix setting smart alarms
#### Version 0.20.0
* Inital Amazfit Bip support (WIP)
* Various theming fixes
* Add workaround for blacklist not properly persisting
* Handle resetting language to default properly
* Pebble: Pass booleans from Javascript Appmessage correctly
* Pebble: Make local configuration pages work on most recent webview implementation
* Pebble: Allow to blacklist calendars
* Add Greek and German transliteration support
* Various visual improvements to charts
#### Version 0.19.4
* Replace or relicense CC-NC licensed icons to satisfy F-Droid
* Mi Band 2: Make infos to display on the Band configurable
* Mi Band 2: Support wrist rotation to switch info setting
* Mi Band 2: Support goal notification setting
* Mi Band 2: Support do not disturb setting
* Mi Band 2: Support inactivity warning setting
#### Version 0.19.3
* Pebble: Fix crash when calendar access permission has been denied
* Pebble: Fix wrong timestamps with Morpheuz running on Firmware >=3
* Mi Band 2: Improve reliability when fetching activity data
* HPlus: Fix intensity calculation without continuous connectivity
* HPlus: Fix Unicode handling
* HPlus: Initial not work detection
* Fix memory leak
* Only show Realtime Chart on devices supporting it
#### Version 0.19.2
* Pebble: Fix recurring calendar events only appearing once per week
* HPlus: Fix crash when receiving calls without phone number
* HPlus: Detect unicode support on Zeband Plus
* No longer quit Gadgetbridge when bluetooth gets turned off
#### Version 0.19.1
* Fix crash at startup
* HPlus: Improve reconnection to device
* Improve transliteration
###Version 0.19.0
#### Version 0.19.0
* Pebble: allow calendar sync with Timeline (Title, Location, Description)
* Pebble: display calendar icon for reminders from AOSP Calendar
* HPlus: try to fix latin characters showing as random Chinese text
@ -13,7 +57,7 @@
* Improve generic notification reliability by trying to restart the notification listener when stale/crashed
* Other small bugfixes
###Version 0.18.5
#### Version 0.18.5
* Applied some material design guidelines to Charts and (pebble) app management
* Changed colours: deep sleep is now dark blue, light sleep is now light blue
* Support for exporting and importing of preferences in addition to the database
@ -24,24 +68,24 @@
* HPlus: users can now decide whether they want to pair the device or not, hopefully fixing some connection problems (#642)
* HPlus: display battery state and warn on low battery
###Version 0.18.4
#### Version 0.18.4
* Mi Band 2: Display realtime steps in Live Activity
* Mi Band: Attempt to recognize Mi Band model with hwVersion = 8
* Alarms activity improvements and fixes
* Make Buttons in the main activity easier to hit
###Version 0.18.3
#### Version 0.18.3
* Fix bug that caused the same value in weekly charts for every day on Android 6 and older
###Version 0.18.2
#### Version 0.18.2
* Mi Band 2: Fix crash on "chat" or "social network" text notification (#603)
###Version 0.18.1
#### Version 0.18.1
* Pebble: Fix Firmware insstallation on Pebble Time Round (broken since 0.16.0)
* Start VibrationActivity when using "find device" button with Vibratissimo
* Support material fork of K9
###Version 0.18.0
#### Version 0.18.0
* All new GUI for the control center
* Add Portuguese pt_PT and pt_BR translations
* Add Czech translation
@ -57,13 +101,13 @@
* Mi Band 2: Set 12h/24h time format, following the Android configuration (#573)
* Improved BLE discovery and connectivity
####Version 0.17.5
#### Version 0.17.5
* Automatically start the service on boot (can be turned off)
* Pebble: PebbleKit compatibility improvements (Datalogging)
* Pebble: Display music shuffle and repeat states for some players
* Pebble 2/LE: Speed up data transfer
####Version 0.17.4
#### Version 0.17.4
* Better integration with android music players
* Privacy options for calls (hide caller name/number)
* Send a notification to the connected if the Android Alarm Clock rings (com.android.deskclock)
@ -76,23 +120,23 @@
* HPlus: Add device specific preferences and icon
* HPlus: Support for Makibes F68
####Version 0.17.3
#### Version 0.17.3
* HPlus: Improve display of new messages and phone calls
* HPlus: Fix bug related to steps and heart rate
* Pebble: Support dynamic keys for natively supported watchfaces and watchapps (more stability accross versions)
* Pebble: Fix error Toast being displayed when TimeStyle watchface is not installed
* Mi Band 1+2: Support for connecting wihout BT pairing (workaround for certain connection problems)
####Version 0.17.2
#### Version 0.17.2
* Pebble: Fix temperature unit in Timestyle Pebble watchface
* Add optional Cyrillic transliteration (for devices lacking the font)
####Version 0.17.1
#### Version 0.17.1
* Pebble: Fix installation of some watchapps
* Pebble: Try to improve PebbleKit compatibility
* HPlus: Fix bug setting current date
####Version 0.17.0
#### Version 0.17.0
* Add weather support through "Weather Notification" app
* Various fixes for K9 mail when using the generic notification receiver
* Add a preference to hide the persistent notification icon of Gadgetbridge
@ -110,7 +154,7 @@
* HPlus: Experimental synchronization of activity data (only sleep, steps and intensity)
* HPlus: Fix some disconnection issues
####Version 0.16.0
#### Version 0.16.0
* New devices: HPlus (e.g. Zeblaze ZeBand), contributed by João Paulo Barraca
* ZeBand: Initial support: notifications, heart rate, sleep monitoring, user configuration, date+time
* Pebble 2: Fix Pebble Classic FW 3.x app variant being prioritized over native Pebble 2 app variant
@ -121,15 +165,15 @@
* Support sharing firmwares/watchapps/watchfaces to Gadgetbridge
* Support for the "Subsonic" music player (#474)
####Version 0.15.2
#### Version 0.15.2
* Mi Band: Fix crash with unknown notification sources
####Version 0.15.1
#### Version 0.15.1
* Improved handling of notifications for some apps
* Pebble 2/LE: Add setting to limit GATT MTU for debugging broken BLE stacks
* Mi Band 2: Display battery status
####Version 0.15.0
#### Version 0.15.0
* New device: Liveview
* Liveview: initial support (set the time and receive notifications)
* Pebble: log pebble app logs if option is enabled in pebble development settings
@ -137,20 +181,20 @@
* Pebble: Further improve compatibility for watchface configuration
* Mi Band 2: Initial support for firmware update (tested so far: 1.0.0.39)
####Version 0.14.4
#### Version 0.14.4
* Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings
* Mi Band 2: Experimental support for activity recognition
* Mi Band 2: Fix time setting code
####Version 0.14.3
#### Version 0.14.3
* Pebble: Experimental support for pairing and using all Pebble models via BLE
* Mi Band 1: Fix regression causing display of wrong activity data (#440)
* Mi Band 2: Support for continuous heart rate measurements in live activity view
####Version 0.14.2
#### Version 0.14.2
* Pebble 2: Fix a bug where the Pebble got disconnected by other unrelated LE devices
####Version 0.14.1
#### Version 0.14.1
* Mi Band 2: Initial experimental support for activity data
* Mi Band 2: Send the fitness goal (steps) to the band
* Pebble 2: Work around firmware installation issues (tested with upgrading 4.2 to 4.3)
@ -158,7 +202,7 @@
* Pebble: add Kickstart watch face to app manager on FW 4.x
* Charts: display the total time range, not just the range with available data
####Version 0.14.0
#### Version 0.14.0
* Pebble 2: Initial experimental support for P2/PT2 using BLE
* Pebble: Special support in device discovery activity (MUST be used to get Pebble 2 working)
* Pebble: Improve compatibility for watchface configuration
@ -167,16 +211,16 @@
* Mi Band 2: configuration option to display the time + date or just the time
* Mi Band 2: honor the wear location configuration option
####Version 0.13.9
#### Version 0.13.9
* Pebble: use the last known location for setting sunrise and sunset
* Pebble: fix Health disappearing forever when deactivating through app manager (and get it back for affected users)
* Mi Band 2: More fixes for connection issues (#408)
####Version 0.13.8
#### Version 0.13.8
* Mi Band 2: fix connection issues for users of Mi Fit (#408, #425)
* Mi Band 1A: fix firmware update for certain 1A models
####Version 0.13.7
#### Version 0.13.7
* Pebble: Fix configuration of certain pebble apps (eg. QR Generator, Squared 4.0)
* Pebble: Add context menu option in app manager to search a watchapp in the pebble appstore
* Mi Band: allow to delete Mi Band address from development settings
@ -185,16 +229,16 @@
* Attempt to fix spurious device discovery problems
* Correctly recognize Toffeed, Slimsocial and MaterialFBook as facebook notification sources
####Version 0.13.6
#### Version 0.13.6
* Mi Band 2: Support for multiple alarms (3 at the moment)
* Mi Band 2: Fix for alarms not working when just one is enabled
####Version 0.13.5
#### Version 0.13.5
* Mi Band 2: Support setting one alarm
* Pebble: Health compatibility for Firmware 4.2
* Improve support for K9 when generic notifications are used (K9 notifications set to never)
####Version 0.13.4
#### Version 0.13.4
* Mi Band: Initial support for recording heart and displaying rate values
* Mi Band: Support for testing vibration patterns directly from the preferences
* Mi Band: Clean up vibration preferences
@ -205,36 +249,36 @@
* Pebble: new icons and colours for certain apps
* Debug-screen: added button to test "new functionality", currently live sensor data for Mi Band 1
####Version 0.13.3
#### Version 0.13.3
* Fix regressions with missing bars and labels in charts
* Allow to set notification type in Debug activity
* Move "Disconnect" back to the bottom of the context menu
* Mi Band 2: Display Message and Phone icons
####Version 0.13.2
#### Version 0.13.2
* Support deleting devices (and their data) in control center
* Sort devices lexicographically in control center
* Do not forward group summary notifications (could fix some duplicate notifications)
* Pebble: Support for health on FW 4.1
* Mi Band: Fix offline charts not displaying heartrate for Mi 1S
####Version 0.13.1
#### Version 0.13.1
* Improved BLE scanning for Android 5.0+
* Pebble: try to work around duplicate Telegram messages and support Telegram icon
* Pebble: fix some incompatibilities with certain PebbleKit Android apps
####Version 0.13.0
#### Version 0.13.0
* Initial working Mi Band 2 support (only notifications, no activity and heart rate support)
* Experimental support for Vibratissimo devices
####Version 0.12.2
#### Version 0.12.2
* Fix for user attribute database table getting spammed and store sleep and steps goals properly
####Version 0.12.1 (release withdrawn)
#### Version 0.12.1 (release withdrawn)
* Pebble: Fix activity data being associated with the wrong device and/or user in some cases causing them to invisible in charts
* Remove special handling for Conversations notifications since upstream dropped special pebble support
####Version 0.12.0 (release withdrawn)
#### Version 0.12.0 (release withdrawn)
* NB: User action needed to migrate existing data!
* Store activity data per device and provider to allow multiple devices of the same kind with separate data. Migration is available, except for Pebble Misfit data. Existing data from multiple devices of the same kind (eg. multiple Mi Bands) will get merged while importing.
* In Control Center, display known devices even when Bluetooth is off
@ -243,10 +287,10 @@
* Pebble: Optionally allow raw Pebble Health data to be stored in database completely (for later interpretation, when we are able to decode it)
* Mi Band: fix displaying of deep sleep vs. light sleep (was inverted)
####Version 0.11.2
#### Version 0.11.2
* Mi Band: support for devices that cannot pair with the band (#349)
####Version 0.11.1
#### Version 0.11.1
* Various fixes (including crashes) for location settings
* Pebble: Support Pebble Time 2 emulator (needs recompilation of Gadgetbridge)
* Fix a rare crash when, due to Bluetooth problems, when a device has no name
@ -259,19 +303,19 @@
* Charts: only display heart rate samples on devices that support that
* Add more logging to detect problems with external directories (#343)
####Version 0.11.0
#### Version 0.11.0
* Pebble: new App Manager (keeps track of installed apps and allows app sorting on FW 3.x)
* Pebble: call dismissal with canned SMS (FW 3.x)
* Pebble: watchapp configuration presets
* Pebble: fix regression with FW 2.x (almost everything was broken in 0.10.2)
####Version 0.10.2
#### Version 0.10.2
* Pebble: allow to manually paste configuration data for legacy configuration pages
* Pebble: various improvements to the configuration page
* Pebble: Support FW 4.0-dp1 and Pebble2 emulator (needs recompilation of Gadgetbridge)
* Pebble: Fix a problem with key events when using the Pebble music player
####Version 0.10.1
#### Version 0.10.1
* Pebble: set extended music info by dissecting notifications on Android 5.0+
* Pebble: various other improvements to music playback
* Pebble: allow ignoring activity trackers individually (to keep the data on the pebble)
@ -279,7 +323,7 @@
* Mi Band: initial and untested support for Mi Band 2
* Allow setting the application language
####Version 0.10.0
#### Version 0.10.0
* Pebble: option to send sunrise and sunset events to timeline
* Pebble: fix problems with unknown app keys while configuring watchfaces
* Mi Band: BLE connection fixes
@ -287,7 +331,7 @@
* Re-enable device paring activity on Android 6 (BLE scanning needs the location preference)
* Display device address in device info
####Version 0.9.8
#### Version 0.9.8
* Pebble: fix more reconnect issues
* Pebble: fix deep sleep not being detected with Firmware 3.12 when using Pebble Health
* Pebble: option in AppManager to delete files from cache
@ -296,7 +340,7 @@
* Pebble: fix music information being messed up
* Honour "Do Not Disturb" for phone calls and SMS
####Version 0.9.7
#### Version 0.9.7
* Pebble: hopefully fix some reconnect issues
* Mi Band: fix live activity monitoring running forever if back button pressed
* Mi Band: allow low latency firmware updates, fixes update with some phones
@ -304,14 +348,14 @@
* Show aliases for BT Devices if they had been renamed in BT Settings
* Do not show a hint about App Manager when a Mi Band is connected
####Version 0.9.6
#### Version 0.9.6
* Again some UI/theme improvements
* New preference to reconnect after connection loss (defaults to true)
* Fix crash when dealing with certain old preference values
* Mi Band: automatically reconnect when back in range after connection loss
* Mi Band 1S: display heart rate value again when invoked via the Debug view
####Version 0.9.5
#### Version 0.9.5
* Several UI Improvements
* Easier First-time setup by using a FAB
* Optional Dark Theme
@ -321,7 +365,7 @@
* Mi Band 1S: Initial live heartrate tracking
* Fix certain crash in charts activity on slower devices (#277)
####Version 0.9.4
#### Version 0.9.4
* Pebble: support pebble health datalog messages of firmware 3.11 (this adds support for deep sleep!)
* Pebble: try to reconnect on new notifications and phone calls when connection was lost unexpectedly
* Pebble: delay between reconnection attempts (from 1 up to 64 seconds)
@ -331,24 +375,24 @@
* Mi Band 1S: full support for firmware upgrade/downgrade (both for Mi Band and heart rate sensor) (#234)
* Mi Band 1S: fix device detection for certain versions
####Version 0.9.3
#### Version 0.9.3
* Pebble: Fix Pebble Health activation (was not available in the App Manager)
* Simplify connection state display (only connecting->connected)
* Small improvements to the pairing activity
* Mi Band 1S: Fix for mi band firmware update
####Version 0.9.2
#### Version 0.9.2
* Mi Band: Fix update of second (HR) firmware on Mi1S (#234)
* Fix ordering issue of device infos being displayed
####Version 0.9.1
#### Version 0.9.1
* Mi Band: fix sporadic connection problems (stuck on "Initializing" #249)
* Mi Band: enable low latency connection (faster) during initialization and activity sync
* Mi Band: better feedback for firmware update
* Device Item is now clickable also when the information entries are visible
* Fix enabling log file writing #261
####Version 0.9.0
#### Version 0.9.0
* Pebble: Support for configuring watchfaces/apps locally (clay) or though webbrowser (some do not work)
* Pebble: hide the alarm management activity as it's unsupported
* Mi Band: Improve firmware detection and updates, including 1S support
@ -357,19 +401,19 @@
* Do not display activity samples when navigating too far in the past
* Fix auto connect which was broken under some circumstances
####Version 0.8.2
#### Version 0.8.2
* Fix database creation and updates (thanks @feclare)
* Add experimental widget to set the alarm time to a configurable number of hours in the future (thanks @0nse)
* Use ckChangeLog to display the Changelog within Gadgetbridge
* Workaround to fix logfile rotation (bug in logback-android)
####Version 0.8.1
#### Version 0.8.1
* Pebble: install (and start) freshly-installed apps on the watch instead of showing a Toast that tells the user to do so. (only applies to firmware 3.x)
* Pebble: fix crash while receiving Health data
* Mi Band 1S: support for synchronizing activity data (#205)
* Mi Band 1S: support for reading the heart rate via the "Debug Screen" #178
####Version 0.8.0
#### Version 0.8.0
* Pebble: Support Pebble Health: steps/activity data are stored correctly. Sleep time is considered as light sleep. Deep sleep is discarded. The pebble will send data where it seems appropriate, there is no action to perform on the watch for this to happen.
* Pebble: Fix support for newer version of morpheuz (>=3.3?)
* Pebble: Allow to select the preferred activity tracker via settings activity (Health, Misfit, Morpheuz)
@ -379,17 +423,17 @@
* Very basic support Android 6 runtime permission
* Fix layout of the alarms activity
####Version 0.7.4
#### Version 0.7.4
* Refactored the settings activity: User details are now generic instead of miband specific. Old settings are preserved.
* Pebble: Fix regression with broken active reconnect since 0.7.0
* Pebble: Support activation and deactivation of Pebble Health. Activation uses the User details as seen above. Insights are NOT activated.
Please be aware that deactivation does NOT delete the data stored on the watch (but it seems to stop the tracking), and we do not know how to switch to metric length units.
####Version 0.7.3
#### Version 0.7.3
* Pebble: Report connection state to PebbleKit companion apps via content provider. NOTE: Makes Gadgetbridge mutual exclusive with the original Pebble app.
* Ignore generic notification when from SMSSecure when SMS Notifications are on
####Version 0.7.2
#### Version 0.7.2
* Pebble: Allow replying to generic notifications that contain a wearable reply action (tested with Signal)
* Pebble: Support setting up a common suffix for canned replies (defaults to " (canned reply)")
* Mi Band: Avoid NPEs when aborting an erroneous sync #205
@ -397,11 +441,11 @@
* Add a confirmation dialog when performing a db import
* Sort blacklist by package names
####Version 0.7.1
#### Version 0.7.1
* Pebble: allow reinstallation of apps in pbw-cache from App Manager (long press menu)
* Pebble: Fix regression which freezes Gadgetbridge when disconnecting via long-press menu
####Version 0.7.0
#### Version 0.7.0
* Read upcoming events (up to 7 days in the future). Requires READ_CALENDAR permission
* Fix double SMS on Sony Android and Android 6.0
* Pebble: Support replying to SMS form the watch (canned replies)
@ -413,7 +457,7 @@
* Mi Band: Display unique devices Names, not just "MI"
* Some new and updated icons
####Version 0.6.9
#### Version 0.6.9
* Pebble: Store app details in pbw-cache and display them in app manager on firmware 3.x
* Pebble: Increase maximum notification body length from 255 to 512 bytes on firmware 3.x
* Pebble: Support installing .pbl (language files) on firmware 3.x
@ -426,7 +470,7 @@
* Mi Band: KitKat: hopefully fixed showing the progress bar during activity data synchronization (#155)
* Mi Band 1S: hopefully fixed connection errors (#178) Notifications probably do not work yet, though
####Version 0.6.8
#### Version 0.6.8
* Mi Band: support for Firmware upgrade/downgrade on Mi Band 1A (white LEDs, no heartrate sensor)
* Pebble: fix regression in 0.6.7 when installing pbw/pbz files from content providers (eg. download manager)
* Pebble: fix installation of pbw files on firmware 3.x when using content providers (eg. download manager)
@ -434,26 +478,26 @@
+ Treat Signal notifications as chat notifications
* Fix crash when contacts cannot be read on Android 6.0 (non-granted permissions)
####Version 0.6.7
#### Version 0.6.7
* Pebble: Allow installation of 3.x apps on OG Pebble (FW will be released soon)
* Fix crashes on startup when logging is enabled or when entering the app manager on some phones
+ Fix Pebble being detected as MI when unpaired and autoconnect is enabled
* Fix Crash when not having K9 Mail permissions (happens when installing K9 after Gadgetbridge) (#175)
####Version 0.6.6
#### Version 0.6.6
* Mi Band: Huge performance improvement fetching activity data
* Mi Band: attempt at fixing connection problems (#156)
* Pebble: Try to interpret sleep data from Misfit data
* Fix exporting the activity database on devices with read-only external storage (#153)
* Fix totally wrong sleep time in the sleep chart
####Version 0.6.5
#### Version 0.6.5
* Mi Band: Support "Locate Device" with Mi Band 1A (and Mi Band 1 with new firmware)
* Pebble: Support syncing steps from Misfit (untested features must be turned on to see them), intensity=steps, no sleep support yet
* Disable activity fetching when not supported
* Small improvements to live activity charts
####Version 0.6.4
#### Version 0.6.4
* Support pull down to synchronize activity data (#138)
* Display tabs in the Charts activity (#139)
* Mi Band: initial support for Mi Band 1a (the one with white LEDs) (thanks @sarg) (#136)
@ -461,23 +505,23 @@
* Register/unregister BroadcastReceivers instead of enabling/disabling them with PackageManager (#134)
(should fix disconnection because the service is being killed)
####Version 0.6.3
#### Version 0.6.3
* Pebble: support installation of language files (.pbl) on FW 2.x
* Try to prevent service being killed by disallowing backups
####Version 0.6.2
#### Version 0.6.2
* Mi Band: support firmware version 1.0.10.14 (and onwards?) vibration
* Mi Band: get device name from official BT SIG endpoint
* Mi Band: initial support for displaying live activity data, screen stays on
####Version 0.6.1
#### Version 0.6.1
* Pebble: Allow muting (blacklisting) Apps from within generic notifications on the watch
* Pebble: Detect all known Pebble Versions including new "chalk" platform (Pebble Time Round)
* Option to ignore phone calls (useful for Pebble Dialer)
* Mi Band: Added progressbar for activity data transfer and fixes for firmware transfer progressbar
* Bugfix for app blacklist (some checkboxes where wrongly drawn as checked)
####Version 0.6.0
#### Version 0.6.0
* Pebble: WIP implementation of PebbleKit Intents to make some 3rd party Android apps work with the Pebble (eg. Ventoo)
* Pebble: Option to set reconnection attempts in settings (one attempt usually takes about 5 seconds)
* Support controlling all audio players that react to media buttons (can be chosen in settings)
@ -486,13 +530,13 @@
* Allow opening firmware / app files from the download manager "app" (technically a content provider)
* Mi Band: whitelisted a few firmware versions
####Version 0.5.4
#### Version 0.5.4
* Mi Band: allow the transfer of activity data without clearing MiBand's memory
* Pebble: for generic notifications use generic icon instead of SMS icons on FW 3.x (thanks @roidelapluie)
* Pebble: use different icons and background colors for specific groups of applications (chat, mail, etc) (thanks @roidelapluie)
* In settings, support blacklisting apps for generic notifications
####Version 0.5.3
#### Version 0.5.3
* Pebble: For generic notifications, support dismissing individual notifications and "Open on Phone" feature (OG & PT)
* Pebble: Allow to treat K9 notifications as generic notifications (if notification mode is set to never)
* Ignore QKSMS notifications to avoid double notification for incoming SMS
@ -500,7 +544,7 @@
* Device state again visible on lockscreen
* Date display and navigation now working properly for all charts
####Version 0.5.2
#### Version 0.5.2
* Pebble: support "dismiss all" action also on Pebble Time/FW 3.x notifications
* Mi Band: show a notification when the battery is below 10%
* Graphs are now using the same theme as the rest of the application
@ -508,11 +552,11 @@
* Remove unused settings option in charts view
* Build target is now Android SDK 23 (Marshmallow)
####Version 0.5.1
#### Version 0.5.1
* Pebble: support taking screenshot from Pebble Time
* Fix broken "find lost device" which was broken in 0.5.0
####Version 0.5.0
#### Version 0.5.0
* Mi Band: fix setting wear location
* Pebble: experimental watchapp installation support for FW 3.x/Pebble Time
* Pebble: support Pebble emulator via TCP connection (needs rebuild with INTERNET permission)
@ -521,7 +565,7 @@
* Support going forward/backwards in time in the activity charts
* Various small bugfixes to the App/FW Installation Activity
####Version 0.4.6
#### Version 0.4.6
* Mi Band: Fixed negative number of steps displayed (#91)
* Mi Band: fixed (re-) connection problems after band getting disconnected
* Pebble: new option to enable untested code (enable only if you like bad surprises)
@ -531,7 +575,7 @@
* Small firmware installation improvements
* Various refactorings and code cleanups
####Version 0.4.5
#### Version 0.4.5
* Enhancement to activity graphs: new graph showing the number of steps done today and in the last week
* New preference to set the desired fitness goal (number of steps to walk in one day)
* Mi Band: support for setting the fitness goal (the band will show the progress to the goal with the LEDs and vibrates when the goal is reached)
@ -539,7 +583,7 @@
* Mi Band: support for flashing firmware from .fw files (upgrades and downgrades are possible)
* Fixed crash when synchronizing activity data in the graphs activity and changing device orientation
####Version 0.4.4
#### Version 0.4.4
* Set Gadgetbridge notification visibility to public, to show the connection status on the lockscreen
* Support for backup up and restoring of the activity database (via Debug activity)
* Support for graceful upgrades and downgrades, keeping your activity database intact
@ -549,24 +593,24 @@
* Pebble: make FW 3.x notifications available by default
* Mi Band: Set the graphs activity as the default action available with a single tap on the connected device
####Version 0.4.3
#### Version 0.4.3
* Mi Band: Support for setting alarms
* Mi Band: Bugfix for activity data synchronization
####Version 0.4.2
#### Version 0.4.2
* Material style for Lollipop
* Support for finding a lost device (vibrate until cancelled)
* Mi Band: Support for vibration profiles, configurable for notifications
* Pebble: Support taking screenshots from the device context menu (Pebble Time not supported yet)
####Version 0.4.1
#### Version 0.4.1
* New icons, thanks xphnx!
* Improvements to Sleep Monitor charts
* Pebble: use new Sleep Monitor for Morpheuz (previously Mi Band only)
* Pebble: experimental support for FW 3.x notification protocol
* Pebble: dev option to force latest notification protocol
####Version 0.4.0
#### Version 0.4.0
* Pebble: Initial Morpheuz protocol support for getting sleep data
* Pebble: Support launching of watchapps though the AppManager Activity
* Pebble: Support CM 12.1 default music app (Eleven)
@ -580,33 +624,33 @@
* Fix Debug activity (SMS and E-Mail buttons were broken)
* Add Turkish translation contributed by Tarik Sekmen
####Version 0.3.5
#### Version 0.3.5
* Add discovery and pairing Activity for Pebble and Mi Band
* Listen for Pebble Message Intents and forward notifications (used by Conversations)
* Make strings translatable and add German, Italian, Russian, Spanish and Korean translations
* Mi Band: Display battery status
####Version 0.3.4
#### Version 0.3.4
* Pebble: Huge speedup for app/firmware installation.
* Pebble: Use a separate notification with progress bar for installation procedure
* Pebble: Bugfix for being stuck while waiting for a slot, when none is available
* Mi Band: Display connection status in notification (previously Pebble only)
####Version 0.3.3
#### Version 0.3.3
* Pebble: Try to reduce battery usage by acknowledging datalog packets
* Mi Band: Set current time on the device (thanks to PR by @danielegobbetti)
* More robust connection state handling and display
####Version 0.3.2
#### Version 0.3.2
* Mi Band: Fix for notifications only working after manual connection
* Mi Band: Display firmware version
* Pebble: Display hardware revision
* Pebble: Check if firmware is compatible before allowing installation
####Version 0.3.1
#### Version 0.3.1
* Mi Band: Fix for notifications only working in Debug
####Version 0.3.0
#### Version 0.3.0
* Mi Band: Initial support (see README.md)
* Pebble: Firmware installation (USE AT YOUR OWN RISK)
* Pebble: Fix installation problems with certain .pbw files
@ -614,7 +658,7 @@
* Add icon for activity tracker apps (icon by xphnx)
* Let the application quit when in reconnecting state
####Version 0.2.0
#### Version 0.2.0
* Experimental pbw installation support (watchfaces/apps)
* New icons for device and app lists
* Fix for device list not refreshing when Bluetooth gets turned on
@ -622,31 +666,31 @@
* Fix for crash on some devices when creating a debug notification
* Lots of internal changes preparing multi device support
####Version 0.1.5
#### Version 0.1.5
* Fix for DST (summer time)
* Option to sync time on connect (enabled by default)
* Opening .pbw files with Gadgetbridge prints some package information
(This was not meant to be released yet, but the DST fix made a new release necessary)
####Version 0.1.4
#### Version 0.1.4
* New AppManager shows installed Apps/Watchfaces (removal possible via context menu)
* Allow back navigation in ActionBar (Debug and AppMananger Activities)
* Make sure Intent broadcasts do not leave Gadgetbridge
* Show hint in the Main Activity (tap to connect etc)
####Version 0.1.3
#### Version 0.1.3
* Remove the connect button, list all supported devices and connect on tap instead
* Display connection status and firmware of connected devices in the device list
* Remove quit button from the service notification, put a quit item in the context menu instead
####Version 0.1.2
#### Version 0.1.2
* Added option to start Gadgetbridge and connect automatically when Bluetooth is turned on
* stop service if Bluetooth is turned off
* try to reconnect if connection was lost
####Version 0.1.1
#### Version 0.1.1
* Fixed various bugs regarding K-9 Mail notifications.
* "Generic notification support" in Setting now opens Androids "Notification access" dialog.
####Version 0.1.0
#### Version 0.1.0
* Initial release

View File

@ -1,21 +1,27 @@
The following artwork is licensed under the following licenses
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0):
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
ic_launcher.png
ic_device_pebble.png
ic_device_miband.png
ic_device_lovetoy.png
ic_device_hplus.png
ic_device_default.png
ic_activitytracker.png
ic_watchface.png
ic_languagepack.png
ic_firmware.png
ic_watchapp.png
ic_systemapp.png
icon.png (fastlane metadata directories)
featureGraphic.png (fastlane metadata directories)
(All of the above by xphnx)
Creative Commons Attribution-NonCommercial-ShareAlike (CC BY-NC-SA):
ic_launcher.png (by Joseph Kim)
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
"GET IT ON F-Droid" button by Laura Kalbag. Source: https://ind.ie/about/blog/f-droid-button/
Creative Commons Attribution 3.0 Unported license (CC BY-3.0):
ic_notification_battery_low.png by Picol.org. Source: https://commons.wikimedia.org/wiki/File:Battery_1_Picol_icon.svg
Creative Commons Attribution 3.0 United States (CC BY-3.0 US):
ic_donate by Peter van Driel https://thenounproject.com/term/donate/239009/

View File

@ -2,8 +2,13 @@ Gadgetbridge
============
Gadgetbridge is an Android (4.4+) application which will allow you to use your
Pebble or Mi Band without the vendor's closed source application and without the
need to create an account and transmit any of your data to the vendor's servers.
Pebble, Mi Band, Amazfit Bit and HPlus device (and more) without the vendor's closed source application
and without the need to create an account and transmit any of your data to the
vendor's servers.
[Homepage](https://gadgetbridge.org)
[Blog](https://blog.gadgetbridge.org)
[![Build](https://travis-ci.org/Freeyourgadget/Gadgetbridge.svg?branch=master)](https://travis-ci.org/Freeyourgadget/Gadgetbridge)
@ -11,16 +16,17 @@ need to create an account and transmit any of your data to the vendor's servers.
[![Gadgetbridge on F-Droid](/Get_it_on_F-Droid.svg.png?raw=true "Download from F-Droid")](https://f-droid.org/repository/browse/?fdid=nodomain.freeyourgadget.gadgetbridge)
[List of changes](CHANGELOG.md)
[List of changes](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/CHANGELOG.md)
## Supported Devices
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 (add the device from within Gadgetbridge!) [Wiki section about pebble](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble), most parts apply to Pebble 2 as well
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki section about mi band](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), some parts apply to mi band 2 as well
* Vibratissimo (experimental)
* Liveview
* Amazfit Bip (WIP) [Wiki section about the Amazfit Bip](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* HPlus Devices (e.g. ZeBand) [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* Liveview
* Vibratissimo (experimental)
## Features (Pebble)
@ -141,7 +147,7 @@ Contributions are welcome, be it feedback, bugreports, documentation, translatio
on any of the open [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues?q=is%3Aopen+is%3Aissue);
just leave a comment that you're working on one to avoid duplicated work.
Translations can be contributed via https://www.transifex.com/projects/p/gadgetbridge/resource/strings/ or manually.
Translations can be contributed via https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/
## Do you have further questions or feedback?

View File

@ -26,8 +26,8 @@ android {
targetSdkVersion 25
// note: always bump BOTH versionCode and versionName!
versionName "0.19.1"
versionCode 94
versionName "0.20.2"
versionCode 100
vectorDrawables.useSupportLibrary = true
}
buildTypes {

View File

@ -63,6 +63,10 @@
android:name=".activities.AppBlacklistActivity"
android:label="@string/title_activity_appblacklist"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.CalBlacklistActivity"
android:label="@string/title_activity_calblacklist"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.FwAppInstallerActivity"
android:theme="@style/GadgetbridgeTheme.NoActionBar"
@ -110,6 +114,26 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\.res" />
<data android:pathPattern="/.*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\.gps" />
<data android:pathPattern="/.*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
@ -183,6 +207,26 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\.res" />
<data android:pathPattern="/.*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\.gps" />
<data android:pathPattern="/.*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />

View File

@ -9,6 +9,18 @@
<script type="text/javascript">
</script>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
iframe {
display: block;
width: 100%;
height: 100%;
border: none;
}
body {
-webkit-user-select: none;
-moz-user-select: none;

View File

@ -75,6 +75,13 @@ function showStep(desiredStep) {
}
}
function hideSteps() {
var steps = document.getElementsByClassName("step");
for (var i = 0; i < steps.length; i ++) {
steps[i].style.display = 'none';
}
}
function gbPebble() {
this.configurationURL = null;
this.configurationValues = null;
@ -142,7 +149,18 @@ function gbPebble() {
self.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
} else {
//TODO: add custom return_to
location.href = url;
var iframe = document.getElementsByTagName('iframe')[0];
var oldbody = document.getElementsByTagName("body")[0];
if (iframe === undefined && oldbody !== undefined) {
iframe = document.createElement("iframe");
oldbody.parentNode.replaceChild(iframe,oldbody);
} else {
hideSteps();
document.documentElement.appendChild(iframe);
}
iframe.src = url;
}
}
@ -155,11 +173,15 @@ function gbPebble() {
try {
self.configurationValues = JSON.stringify(dict);
document.getElementById("jsondata").innerHTML=self.configurationValues;
return callbackAck;
if (callbackAck != undefined) {
callbackAck();
}
}
catch (e) {
GBjs.gbLog("sendAppMessage failed");
return callbackNack;
if (callbackNack != undefined) {
callbackNack();
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -24,6 +24,7 @@ import android.app.NotificationManager.Policy;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@ -40,6 +41,7 @@ import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
@ -88,6 +90,7 @@ public class GBApplication extends Application {
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change";
private static GBApplication app;
@ -102,6 +105,7 @@ public class GBApplication extends Application {
}
}
};
private static Locale language;
private DeviceManager deviceManager;
@ -157,9 +161,12 @@ public class GBApplication extends Application {
setupExceptionHandler();
deviceManager = new DeviceManager(this);
String language = prefs.getString("language", "default");
setLanguage(language);
deviceService = createDeviceService();
loadBlackList();
loadAppsBlackList();
loadCalendarsBlackList();
if (isRunningMarshmallowOrLater()) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@ -335,47 +342,107 @@ public class GBApplication extends Application {
return NotificationManager.INTERRUPTION_FILTER_ALL;
}
private static HashSet<String> blacklist = null;
private static HashSet<String> apps_blacklist = null;
public static boolean isBlacklisted(String packageName) {
return blacklist != null && blacklist.contains(packageName);
public static boolean appIsBlacklisted(String packageName) {
if (apps_blacklist == null) {
GB.log("appIsBlacklisted: apps_blacklist is null!", GB.INFO, null);
}
return apps_blacklist != null && apps_blacklist.contains(packageName);
}
public static void setBlackList(Set<String> packageNames) {
public static void setAppsBlackList(Set<String> packageNames) {
if (packageNames == null) {
blacklist = new HashSet<>();
GB.log("Set null apps_blacklist", GB.INFO, null);
apps_blacklist = new HashSet<>();
} else {
blacklist = new HashSet<>(packageNames);
apps_blacklist = new HashSet<>(packageNames);
}
saveBlackList();
GB.log("New apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
saveAppsBlackList();
}
private static void loadBlackList() {
blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (blacklist == null) {
blacklist = new HashSet<>();
private static void loadAppsBlackList() {
GB.log("Loading apps_blacklist", GB.INFO, null);
apps_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (apps_blacklist == null) {
apps_blacklist = new HashSet<>();
}
GB.log("Loaded apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
}
private static void saveBlackList() {
private static void saveAppsBlackList() {
GB.log("Saving apps_blacklist with " + apps_blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (blacklist.isEmpty()) {
if (apps_blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
} else {
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, blacklist);
Prefs.putStringSet(editor, GBPrefs.PACKAGE_BLACKLIST, apps_blacklist);
}
editor.apply();
}
public static void addToBlacklist(String packageName) {
if (blacklist.add(packageName)) {
saveBlackList();
public static void addAppToBlacklist(String packageName) {
if (apps_blacklist.add(packageName)) {
saveAppsBlackList();
}
}
public static synchronized void removeFromBlacklist(String packageName) {
blacklist.remove(packageName);
saveBlackList();
public static synchronized void removeFromAppsBlacklist(String packageName) {
GB.log("Removing from apps_blacklist: " + packageName, GB.INFO, null);
apps_blacklist.remove(packageName);
saveAppsBlackList();
}
private static HashSet<String> calendars_blacklist = null;
public static boolean calendarIsBlacklisted(String calendarDisplayName) {
if (calendars_blacklist == null) {
GB.log("calendarIsBlacklisted: calendars_blacklist is null!", GB.INFO, null);
}
return calendars_blacklist != null && calendars_blacklist.contains(calendarDisplayName);
}
public static void setCalendarsBlackList(Set<String> calendarNames) {
if (calendarNames == null) {
GB.log("Set null apps_blacklist", GB.INFO, null);
calendars_blacklist = new HashSet<>();
} else {
calendars_blacklist = new HashSet<>(calendarNames);
}
GB.log("New calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
saveCalendarsBlackList();
}
public static void addCalendarToBlacklist(String calendarDisplayName) {
if (calendars_blacklist.add(calendarDisplayName)) {
saveCalendarsBlackList();
}
}
public static void removeFromCalendarBlacklist(String calendarDisplayName) {
calendars_blacklist.remove(calendarDisplayName);
saveCalendarsBlackList();
}
private static void loadCalendarsBlackList() {
GB.log("Loading calendars_blacklist", GB.INFO, null);
calendars_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
if (calendars_blacklist == null) {
calendars_blacklist = new HashSet<>();
}
GB.log("Loaded calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
}
private static void saveCalendarsBlackList() {
GB.log("Saving calendars_blacklist with " + calendars_blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (calendars_blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
} else {
Prefs.putStringSet(editor, GBPrefs.CALENDAR_BLACKLIST, calendars_blacklist);
}
editor.apply();
}
/**
@ -459,6 +526,23 @@ public class GBApplication extends Application {
editor.apply();
}
public static void setLanguage(String lang) {
if (lang.equals("default")) {
language = Resources.getSystem().getConfiguration().locale;
} else {
language = new Locale(lang);
}
Configuration config = new Configuration();
config.setLocale(language);
// FIXME: I have no idea what I am doing
context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
Intent intent = new Intent();
intent.setAction(ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public static LimitedQueue getIDSenderLookup() {
return mIDSenderLookup;
}
@ -496,4 +580,8 @@ public class GBApplication extends Application {
public static GBApplication app() {
return app;
}
public static Locale getLanguage() {
return language;
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -17,20 +17,28 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.text.InputType;
import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
/**
* A settings activity with support for preferences directly displaying their value.
@ -42,6 +50,22 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage());
break;
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
@ -110,6 +134,12 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
} else {
setTheme(R.style.GadgetbridgeTheme);
}
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
super.onCreate(savedInstanceState);
}
@ -127,6 +157,12 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
/**
* Subclasses should reimplement this to return the keys of those
* preferences which should print its values as a summary below the
@ -178,4 +214,8 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
return super.onOptionsItemSelected(item);
}
private void setLanguage(Locale language) {
AndroidUtils.setLanguage(this, language);
}
}

View File

@ -17,13 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
@ -32,7 +27,6 @@ import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
@ -42,16 +36,6 @@ public class AppBlacklistActivity extends GBActivity {
private AppBlacklistAdapter appBlacklistAdapter;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(GBApplication.ACTION_QUIT)) {
finish();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -78,10 +62,6 @@ public class AppBlacklistActivity extends GBActivity {
return true;
}
});
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
}
@Override
@ -93,10 +73,4 @@ public class AppBlacklistActivity extends GBActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -0,0 +1,152 @@
/* Copyright (C) 2017 Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class CalBlacklistActivity extends GBActivity {
private final String[] EVENT_PROJECTION = new String[]{
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.CALENDAR_COLOR
};
private ArrayList<Calendar> calendarsArrayList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calblacklist);
ListView calListView = (ListView) findViewById(R.id.calListView);
final Uri uri = CalendarContract.Calendars.CONTENT_URI;
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
GB.toast(this, "Calendar permission not granted. Nothing to do.", Toast.LENGTH_SHORT, GB.WARN);
return;
}
try (Cursor cur = getContentResolver().query(uri, EVENT_PROJECTION, null, null, null)) {
calendarsArrayList = new ArrayList<>();
while (cur != null && cur.moveToNext()) {
calendarsArrayList.add(new Calendar(cur.getString(0), cur.getInt(1)));
}
}
ArrayAdapter<Calendar> calAdapter = new CalendarListAdapter(this, calendarsArrayList);
calListView.setAdapter(calAdapter);
calListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int i, long id) {
Calendar item = calendarsArrayList.get(i);
CheckBox selected = (CheckBox) view.findViewById(R.id.item_checkbox);
toggleEntry(view);
if (selected.isChecked()) {
GBApplication.addCalendarToBlacklist(item.displayName);
} else {
GBApplication.removeFromCalendarBlacklist(item.displayName);
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void toggleEntry(View view) {
TextView name = (TextView) view.findViewById(R.id.calendar_name);
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
name.setPaintFlags(name.getPaintFlags() ^ Paint.STRIKE_THRU_TEXT_FLAG);
checked.toggle();
}
class Calendar {
private final String displayName;
private final int color;
public Calendar(String displayName, int color) {
this.displayName = displayName;
this.color = color;
}
}
private class CalendarListAdapter extends ArrayAdapter<Calendar> {
CalendarListAdapter(@NonNull Context context, @NonNull List<Calendar> calendars) {
super(context, 0, calendars);
}
@NonNull
@Override
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
Calendar item = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) super.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item_cal_blacklist, parent, false);
}
View color = view.findViewById(R.id.calendar_color);
TextView name = (TextView) view.findViewById(R.id.calendar_name);
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
if (GBApplication.calendarIsBlacklisted(item.displayName) && !checked.isChecked()) {
toggleEntry(view);
}
color.setBackgroundColor(item.color);
name.setText(item.displayName);
return view;
}
}
}

View File

@ -25,6 +25,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -49,6 +50,7 @@ import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -56,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -80,6 +83,9 @@ public class ControlCenterv2 extends AppCompatActivity
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage());
break;
case GBApplication.ACTION_QUIT:
finish();
break;
@ -170,6 +176,7 @@ public class ControlCenterv2 extends AppCompatActivity
registerForContextMenu(deviceListView);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
@ -189,7 +196,7 @@ public class ControlCenterv2 extends AppCompatActivity
checkAndRequestPermissions();
}
ChangeLog cl = new ChangeLog(this);
ChangeLog cl = createChangeLog();
if (cl.isFirstRun()) {
cl.getLogDialog().show();
}
@ -203,6 +210,13 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
@Override
protected void onDestroy() {
unregisterForContextMenu(deviceListView);
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
@ -235,8 +249,13 @@ public class ControlCenterv2 extends AppCompatActivity
case R.id.action_quit:
GBApplication.quit();
return true;
case R.id.donation_link:
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
return true;
case R.id.external_changelog:
ChangeLog cl = new ChangeLog(this);
ChangeLog cl = createChangeLog();
cl.getFullLogDialog().show();
return true;
}
@ -244,6 +263,14 @@ public class ControlCenterv2 extends AppCompatActivity
return true;
}
private ChangeLog createChangeLog() {
String css = ChangeLog.DEFAULT_CSS;
css += "body { "
+ "color: " + AndroidUtils.getTextColorHex(getBaseContext()) + "; "
+ "background-color: " + AndroidUtils.getBackgroundColorHex(getBaseContext()) + ";" +
"}";
return new ChangeLog(this, css);
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivity.class));
}
@ -288,4 +315,7 @@ public class ControlCenterv2 extends AppCompatActivity
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);
}
private void setLanguage(Locale language) {
AndroidUtils.setLanguage(this, language);
}
}

View File

@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
@ -62,9 +61,6 @@ public class DbManagementActivity extends GBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_db_management);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
dbPath = (TextView) findViewById(R.id.activity_db_management_path);
dbPath.setText(getExternalPath());

View File

@ -78,10 +78,6 @@ public class DebugActivity extends GBActivity {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case GBApplication.ACTION_QUIT: {
finish();
break;
}
case ACTION_REPLY: {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
CharSequence reply = remoteInput.getCharSequence(EXTRA_REPLY);
@ -104,7 +100,6 @@ public class DebugActivity extends GBActivity {
setContentView(R.layout.activity_debug);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
filter.addAction(ACTION_REPLY);
filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);

View File

@ -221,9 +221,6 @@ public class ExternalPebbleJSActivity extends GBActivity {
if (passKey) {
Object obj = in.get(inKey);
if (obj instanceof Boolean) {
obj = ((Boolean) obj) ? "true" : "false";
}
out.put(outKey, obj);
} else {
GB.toast("Discarded key " + inKey + ", not found in the local configuration and is not an integer key.", Toast.LENGTH_SHORT, GB.WARN);

View File

@ -97,9 +97,7 @@ public class FwAppInstallerActivity extends AppCompatActivity implements Install
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GBApplication.ACTION_QUIT.equals(action)) {
finish();
} else if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null) {
refreshBusyState(device);
@ -181,7 +179,6 @@ public class FwAppInstallerActivity extends AppCompatActivity implements Install
detailsListView.setAdapter(mDetailsItemAdapter);
setInstallEnabled(false);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filter.addAction(GB.ACTION_DISPLAY_MESSAGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);

View File

@ -17,44 +17,64 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.LocaleList;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class GBActivity extends AppCompatActivity {
private void setLanguage(String language) {
Locale locale;
if (language.equals("default")) {
locale = Locale.getDefault();
} else {
locale = new Locale(language);
}
Configuration config = new Configuration();
config.locale = locale;
// FIXME: I have no idea what I am doing
getApplicationContext().getResources().updateConfiguration(config, getApplicationContext().getResources().getDisplayMetrics());
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage());
break;
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
private void setLanguage(Locale language) {
AndroidUtils.setLanguage(this, language);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
if (GBApplication.isDarkThemeEnabled()) {
setTheme(R.style.GadgetbridgeThemeDark);
} else {
setTheme(R.style.GadgetbridgeTheme);
}
Prefs prefs = GBApplication.getPrefs();
String language = prefs.getString("language", "default");
setLanguage(language);
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -97,6 +97,15 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
pref = findPreference("pref_key_blacklist_calendars");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, CalBlacklistActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pebble_emu_addr");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -143,6 +152,25 @@ public class SettingsActivity extends AbstractSettingsActivity {
});
pref = findPreference("language");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
String newLang = newVal.toString();
try {
GBApplication.setLanguage(newLang);
} catch (Exception ex) {
GB.toast(getApplicationContext(),
"Error setting language: " + ex.getLocalizedMessage(),
Toast.LENGTH_LONG,
GB.ERROR,
ex);
}
return true;
}
});
if (!GBApplication.isRunningMarshmallowOrLater()) {
pref = findPreference("notification_filter");
PreferenceCategory category = (PreferenceCategory) findPreference("pref_key_notifications");

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -16,12 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.SeekBar;
import org.slf4j.Logger;
@ -33,17 +28,6 @@ import nodomain.freeyourgadget.gadgetbridge.R;
public class VibrationActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(VibrationActivity.class);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case GBApplication.ACTION_QUIT: {
finish();
break;
}
}
}
};
private SeekBar seekBar;
@Override
@ -51,11 +35,6 @@ public class VibrationActivity extends GBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_vibration);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
registerReceiver(mReceiver, filter);
seekBar = (SeekBar) findViewById(R.id.vibration_seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
@ -77,11 +56,4 @@ public class VibrationActivity extends GBActivity {
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}
}

View File

@ -18,17 +18,13 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.view.MenuItem;
import android.view.View;
@ -45,7 +41,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
@ -61,18 +56,6 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
private GBDevice mGBDevice = null;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
public GBDevice getGBDevice() {
return mGBDevice;
}
@ -104,11 +87,6 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
}
});
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
// Set up the ViewPager with the sections adapter.
ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
if (viewPager != null) {
@ -232,10 +210,4 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
}
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -487,7 +487,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
activityEntries.add(createBarEntry(value, ts));
if (hr && isValidHeartRateValue(sample.getHeartRate())) {
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 60*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
heartrateEntries.add(createLineEntry(0, ts - 1));
}
@ -530,7 +530,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
List<IBarDataSet> list = new ArrayList<>();
list.add(activitySet);
BarData barData = new BarData(list);
barData.setBarWidth(100f);
barData.setBarWidth(200f);
// barData.setGroupSpace(0);
combinedData.setData(barData);
@ -595,10 +595,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
protected LineDataSet createHeartrateSet(List<Entry> values, String label) {
LineDataSet set1 = new LineDataSet(values, label);
set1.setLineWidth(0.8f);
set1.setLineWidth(2.2f);
set1.setColor(HEARTRATE_COLOR);
// set1.setDrawCubic(true);
set1.setMode(LineDataSet.Mode.CUBIC_BEZIER);
set1.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
set1.setCubicIntensity(0.1f);
set1.setDrawCircles(false);
// set1.setCircleRadius(2f);

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
Gobbetti, Vebryn
This file is part of Gadgetbridge.
@ -17,7 +17,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
@ -25,6 +30,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class ActivityAnalysis {
private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class);
// store raw steps and duration
protected HashMap<Integer, Long> stats = new HashMap<Integer, Long>();
// max speed determined from samples
private int maxSpeed = 0;
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
@ -53,7 +65,7 @@ class ActivityAnalysis {
int steps = sample.getSteps();
if (steps > 0) {
amount.addSteps(sample.getSteps());
amount.addSteps(steps);
}
if (previousSample != null) {
@ -65,6 +77,22 @@ class ActivityAnalysis {
previousAmount.addSeconds(sharedTimeDifference);
amount.addSeconds(sharedTimeDifference);
}
// add time
if (steps > 0 && sample.getKind() == ActivityKind.TYPE_ACTIVITY) {
if (steps > maxSpeed) {
maxSpeed = steps;
}
if (!stats.containsKey(steps)) {
LOG.info("Adding: " + steps);
stats.put(steps, timeDifference);
} else {
long time = stats.get(steps);
LOG.info("Updating: " + steps + " " + timeDifference + time);
stats.put(steps, timeDifference + time);
}
}
}
previousAmount = amount;

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
Gobbetti, Vebryn
This file is part of Gadgetbridge.
@ -102,9 +102,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_QUIT:
finish();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
refreshBusyState(dev);
@ -136,7 +133,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
initDates();
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
@ -333,6 +329,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return new WeekStepsChartFragment();
case 4:
return new LiveActivityFragment();
case 5:
return new SpeedZonesFragment();
}
return null;
@ -340,7 +338,11 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
@Override
public int getCount() {
// Show 5 total pages.
// Show 5 or 6 total pages.
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
if (coordinator.supportsRealtimeData()) {
return 6;
}
return 5;
}
@ -357,6 +359,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return getString(R.string.weekstepschart_steps_a_week);
case 4:
return getString(R.string.liveactivity_live_activity);
case 5:
return getString(R.string.stats_title);
}
return super.getPageTitle(position);
}

View File

@ -0,0 +1,170 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Vebryn
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.HorizontalBarChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class SpeedZonesFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(SpeedZonesFragment.class);
private HorizontalBarChart mStatsChart;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<? extends ActivitySample> samples = getSamples(db, device);
MySpeedZonesData mySpeedZonesData = refreshStats(samples);
return new MyChartsData(mySpeedZonesData);
}
private MySpeedZonesData refreshStats(List<? extends ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
analysis.calculateActivityAmounts(samples);
BarData data = new BarData();
data.setValueTextColor(CHART_TEXT_COLOR);
List<BarEntry> entries = new ArrayList<>();
ActivityUser user = new ActivityUser();
/*double distanceFactorCm;
if (user.getGender() == user.GENDER_MALE){
distanceFactorCm = user.getHeightCm() * user.GENDER_MALE_DISTANCE_FACTOR / 1000;
} else {
distanceFactorCm = user.getHeightCm() * user.GENDER_FEMALE_DISTANCE_FACTOR / 1000;
}*/
for (Map.Entry<Integer, Long> entry : analysis.stats.entrySet()) {
entries.add(new BarEntry(entry.getKey(), entry.getValue() / 60));
}
BarDataSet set = new BarDataSet(entries, "");
set.setValueTextColor(CHART_TEXT_COLOR);
set.setColors(getColorFor(ActivityKind.TYPE_ACTIVITY));
//set.setDrawValues(false);
//data.setBarWidth(0.1f);
data.addDataSet(set);
return new MySpeedZonesData(data);
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
mStatsChart.setData(mcd.getChartsData().getBarData());
}
@Override
public String getTitle() {
return getString(R.string.stats_title);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_statschart, container, false);
mStatsChart = (HorizontalBarChart) rootView.findViewById(R.id.statschart);
setupStatsChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
private void setupStatsChart() {
mStatsChart.setBackgroundColor(BACKGROUND_COLOR);
mStatsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mStatsChart.setNoDataText("");
mStatsChart.getLegend().setEnabled(false);
mStatsChart.setTouchEnabled(false);
mStatsChart.getDescription().setText("");
XAxis x = mStatsChart.getXAxis();
x.setTextColor(CHART_TEXT_COLOR);
YAxis yr = mStatsChart.getAxisRight();
yr.setTextColor(CHART_TEXT_COLOR);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
@Override
protected void setupLegend(Chart chart) {
// no legend here, it is all about the steps here
chart.getLegend().setEnabled(false);
}
@Override
protected void renderCharts() {
mStatsChart.invalidate();
}
private static class MySpeedZonesData extends ChartsData {
private final BarData barData;
MySpeedZonesData(BarData barData) {
this.barData = barData;
}
BarData getBarData() {
return barData;
}
}
private static class MyChartsData extends ChartsData {
private final MySpeedZonesData chartsData;
MyChartsData(MySpeedZonesData chartsData) {
this.chartsData = chartsData;
}
MySpeedZonesData getChartsData() {
return chartsData;
}
}
}

View File

@ -62,7 +62,7 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapte
if (name == null) {
name = ai.packageName;
}
if (GBApplication.isBlacklisted(ai.packageName)) {
if (GBApplication.appIsBlacklisted(ai.packageName)) {
// sort blacklisted first by prefixing with a '!'
name = "!" + name;
}
@ -94,7 +94,7 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapte
holder.deviceAppNameLabel.setText(mNameMap.get(appInfo));
holder.deviceImageView.setImageDrawable(appInfo.loadIcon(mPm));
holder.checkbox.setChecked(GBApplication.isBlacklisted(appInfo.packageName));
holder.checkbox.setChecked(GBApplication.appIsBlacklisted(appInfo.packageName));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
@ -102,9 +102,9 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapte
CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox));
checkBox.toggle();
if (checkBox.isChecked()) {
GBApplication.addToBlacklist(appInfo.packageName);
GBApplication.addAppToBlacklist(appInfo.packageName);
} else {
GBApplication.removeFromBlacklist(appInfo.packageName);
GBApplication.removeFromAppsBlacklist(appInfo.packageName);
}
}
});

View File

@ -189,6 +189,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
public void onClick(View v) {
Intent startIntent;
startIntent = new Intent(context, ConfigureAlarms.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
}

View File

@ -201,8 +201,6 @@ public interface DeviceCoordinator {
*/
boolean supportsHeartRateMeasurement(GBDevice device);
int getTapString();
/**
* Returns the readable name of the manufacturer.
*/
@ -234,4 +232,10 @@ public interface DeviceCoordinator {
*/
boolean supportsCalendarEvents();
/**
* Indicates whether the device supports getting a stream of live data.
* This can be live HR, steps etc.
*/
boolean supportsRealtimeData();
}

View File

@ -157,11 +157,6 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public int getTapString() {
return 0;
}
@Override
public String getManufacturer() {
return "unknown";
@ -181,4 +176,9 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
}

View File

@ -0,0 +1,60 @@
/* Copyright (C) 2017 Andreas Shimokawa, João Paulo Barraca
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class AmazfitBipCooordinator extends MiBand2Coordinator {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipCooordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.AMAZFITBIP;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit Bip Watch")) {
return DeviceType.AMAZFITBIP;
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
AmazfitBipFWInstallHandler handler = new AmazfitBipFWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
}

View File

@ -0,0 +1,90 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.AmazfitBipFirmwareInfo;
public class AmazfitBipFWHelper extends AbstractMiBandFWHelper {
public AmazfitBipFWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
private AmazfitBipFirmwareInfo firmwareInfo;
@Override
public String format(int version) {
return AmazfitBipFirmwareInfo.toVersion(version);
}
@Override
public int getFirmwareVersion() {
return firmwareInfo.getFirmwareVersion();
}
@Override
public int getFirmware2Version() {
return 0;
}
@Override
public String getHumanFirmwareVersion2() {
return "";
}
@Override
protected int[] getWhitelistedFirmwareVersions() {
return AmazfitBipFirmwareInfo.getWhitelistedVersions();
}
@Override
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
return firmwareInfo.isGenerallyCompatibleWith(device);
}
@Override
public boolean isSingleFirmware() {
return true;
}
@NonNull
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new AmazfitBipFirmwareInfo(wholeFirmwareBytes);
if (!firmwareInfo.isHeaderValid()) {
throw new IllegalArgumentException("Not a an Amazifit Bip firmware");
}
}
@Override
public void checkValid() throws IllegalArgumentException {
firmwareInfo.checkValid();
}
public AmazfitBipFirmwareInfo getFirmwareInfo() {
return firmwareInfo;
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
class AmazfitBipFWInstallHandler extends AbstractMiBandFWInstallHandler {
AmazfitBipFWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new AmazfitBipFWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.AMAZFITBIP;
}
}

View File

@ -0,0 +1,97 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
public class AmazfitBipIcon {
// icons which are unsure which app they are for are suffixed with _NN
public static final int CHAT = 0;
public static final int PENGUIN_1 = 1;
public static final int MI_CHAT_2 = 2;
public static final int FACEBOOK = 3;
public static final int TWITTER = 4;
public static final int MI_APP_5 = 5;
public static final int SNAPCHAT = 6;
public static final int WHATSAPP = 7;
public static final int RED_WHITE_FIRE_8 = 8;
public static final int CHINESE_9 = 9;
public static final int ALARM_CLOCK = 10;
public static final int APP_11 = 11;
public static final int CAMERA_12 = 12;
public static final int CHAT_BLUE_13 = 13;
public static final int COW_14 = 14;
public static final int CHINESE_15 = 15;
public static final int CHINESE_16 = 16;
public static final int STAR_17 = 17;
public static final int APP_18 = 18;
public static final int CHINESE_19 = 19;
public static final int CHINESE_20 = 20;
public static final int CALENDAR = 21;
public static final int FACEBOOK_MESSENGER = 22;
public static final int WHATSAPP_CALL_23 = 23;
public static final int LINE = 24;
public static final int TELEGRAM = 25;
public static final int KAKAOTALK = 26;
public static final int SKYPE = 27;
public static final int VKONTAKTE = 28;
public static final int POKEMONGO = 29;
public static final int HANGOUTS = 30;
public static final int MI_31 = 31;
public static final int CHINESE_32 = 32;
public static final int CHINESE_33 = 33;
public static final int EMAIL = 34;
public static final int WEATHER = 35;
public static final int HR_WARNING_36 = 36;
public static int mapToIconId(NotificationType type) {
switch (type) {
case UNKNOWN:
return APP_11;
case CONVERSATIONS:
return CHAT;
case GENERIC_EMAIL:
return EMAIL;
case GENERIC_NAVIGATION:
return APP_11;
case GENERIC_SMS:
return CHAT;
case GENERIC_CALENDAR:
return CALENDAR;
case FACEBOOK:
return FACEBOOK;
case FACEBOOK_MESSENGER:
return FACEBOOK_MESSENGER;
case RIOT:
return CHAT;
case SIGNAL:
return CHAT_BLUE_13;
case TWITTER:
return TWITTER;
case TELEGRAM:
return TELEGRAM;
case WHATSAPP:
return WHATSAPP;
case GENERIC_ALARM_CLOCK:
return ALARM_CLOCK;
}
return APP_11;
}
}

View File

@ -0,0 +1,24 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import java.util.UUID;
public class AmazfitBipService {
public static final UUID UUID_CHARACTERISTIC_WEATHER = UUID.fromString("0000000e-0000-3512-2118-0009af100700");
}

View File

@ -0,0 +1,160 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
public class AmazfitBipWeatherConditions {
public static final byte CLEAR_SKY = 0;
public static final byte SCATTERED_CLOUDS = 1;
public static final byte CLOUDY = 2;
public static final byte RAIN_WITH_SUN = 3;
public static final byte THUNDERSTORM = 4;
public static final byte HAIL = 5;
public static final byte RAIN_AND_SNOW = 6;
public static final byte LIGHT_RAIN = 7;
public static final byte MEDIUM_RAIN = 8;
public static final byte HEAVY_RAIN = 9;
public static final byte EXTREME_RAIN = 10;
public static final byte SUPER_EXTREME_RAIN = 11;
public static final byte TORRENTIAL_RAIN = 12;
public static final byte SNOW_AND_SUN = 13;
public static final byte LIGHT_SNOW = 14;
public static final byte MEDIUM_SNOW = 15;
public static final byte HEAVY_SNOW = 16;
public static final byte EXTREME_SNOW = 17;
public static final byte MIST = 18;
public static final byte DRIZZLE = 19;
public static final byte WIND_AND_RAIN = 20;
// 21- various types of rain
public static byte mapToAmazfitBipWeatherCode(int openWeatherMapCondition) {
// openweathermap.org conditions:
// http://openweathermap.org/weather-conditions
switch (openWeatherMapCondition) {
//Group 2xx: Thunderstorm
case 200: //thunderstorm with light rain: //11d
case 201: //thunderstorm with rain: //11d
case 202: //thunderstorm with heavy rain: //11d
case 210: //light thunderstorm:: //11d
case 211: //thunderstorm: //11d
case 230: //thunderstorm with light drizzle: //11d
case 231: //thunderstorm with drizzle: //11d
case 232: //thunderstorm with heavy drizzle: //11d
case 212: //heavy thunderstorm: //11d
case 221: //ragged thunderstorm: //11d
return THUNDERSTORM;
//Group 3xx: Drizzle
case 300: //light intensity drizzle: //09d
case 301: //drizzle: //09d
case 302: //heavy intensity drizzle: //09d
case 310: //light intensity drizzle rain: //09d
case 311: //drizzle rain: //09d
case 312: //heavy intensity drizzle rain: //09d
case 313: //shower rain and drizzle: //09d
case 314: //heavy shower rain and drizzle: //09d
case 321: //shower drizzle: //09d
return DRIZZLE;
//Group 5xx: Rain
case 500: //light rain: //10d
return LIGHT_RAIN;
case 501: //moderate rain: //10d
return MEDIUM_RAIN;
case 502: //heavy intensity rain: //10d
return HEAVY_RAIN;
case 503: //very heavy rain: //10d
return EXTREME_RAIN;
case 504: //extreme rain: //10d
return TORRENTIAL_RAIN;
case 511: //freezing rain: //13d
return MEDIUM_RAIN;
case 520: //light intensity shower rain: //09d
return LIGHT_RAIN;
case 521: //shower rain: //09d
return MEDIUM_RAIN;
case 522: //heavy intensity shower rain: //09d
return HEAVY_RAIN;
case 531: //ragged shower rain: //09d
return MEDIUM_RAIN;
//Group 6xx: Snow
case 600: //light snow: //[[file:13d.png]]
return LIGHT_SNOW;
case 601: //snow: //[[file:13d.png]]
return MEDIUM_SNOW;
case 602: //heavy snow: //[[file:13d.png]]
return HEAVY_SNOW;
case 611: //sleet: //[[file:13d.png]]
case 612: //shower sleet: //[[file:13d.png]]
case 615: //light rain and snow: //[[file:13d.png]]
case 616: //rain and snow: //[[file:13d.png]]
case 620: //light shower snow: //[[file:13d.png]]
case 621: //shower snow: //[[file:13d.png]]
case 622: //heavy shower snow: //[[file:13d.png]]
return RAIN_AND_SNOW;
//Group 7xx: Atmosphere
case 701: //mist: //[[file:50d.png]]
case 711: //smoke: //[[file:50d.png]]
case 721: //haze: //[[file:50d.png]]
case 731: //sandcase dust whirls: //[[file:50d.png]]
case 741: //fog: //[[file:50d.png]]
case 751: //sand: //[[file:50d.png]]
case 761: //dust: //[[file:50d.png]]
case 762: //volcanic ash: //[[file:50d.png]]
case 771: //squalls: //[[file:50d.png]]
return MIST;
case 781: //tornado: //[[file:50d.png]]
case 900: //tornado
return WIND_AND_RAIN;
//Group 800: Clear
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
return CLEAR_SKY;
//Group 80x: Clouds
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
return SCATTERED_CLOUDS;
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
return CLOUDY;
//Group 90x: Extreme
case 901: //tropical storm
return WIND_AND_RAIN;
case 903: //cold
case 904: //hot
case 905: //windy
return 0;
case 906: //hail
return HAIL;
//Group 9xx: Additional
case 951: //calm
case 952: //light breeze
case 953: //gentle breeze
case 954: //moderate breeze
case 955: //fresh breeze
case 956: //strong breeze
case 957: //high windcase near gale
case 958: //gale
case 959: //severe gale
case 960: //storm
case 961: //violent storm
case 902: //hurricane
case 962: //hurricane
default:
return 0;
}
}
}

View File

@ -79,7 +79,6 @@ public final class HPlusConstants {
public static final byte ARG_FINDME_ON = 0x01;
public static final byte ARG_FINDME_OFF = 0x02;
public static final byte CMD_GET_VERSION = 0x17;
public static final byte CMD_SET_END = 0x4f;
public static final byte CMD_SET_INCOMING_CALL_NUMBER = 0x23;
public static final byte CMD_SET_ALLDAY_HRM = 0x35;
@ -89,7 +88,8 @@ public final class HPlusConstants {
public static final byte CMD_SET_SIT_INTERVAL = 0x51;
public static final byte CMD_SET_HEARTRATE_STATE = 0x32;
//Actions to device
//GET messages
public static final byte CMD_GET_VERSION = 0x17;
public static final byte CMD_GET_ACTIVE_DAY = 0x27;
public static final byte CMD_GET_DAY_DATA = 0x15;
public static final byte CMD_GET_SLEEP = 0x19;
@ -122,7 +122,7 @@ public final class HPlusConstants {
public static final byte DATA_SLEEP = 0x1A;
public static final byte DATA_VERSION = 0x18;
public static final byte DATA_VERSION1 = 0x2E;
public static final byte DATA_DAY_UNKNOWN = 0x52; //To be defined
public static final byte DATA_UNKNOWN = 0x4d;
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";

View File

@ -92,6 +92,11 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HPLUS;
@ -147,11 +152,6 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Zeblaze";
@ -289,6 +289,6 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}
public static boolean getUnicodeSupport(String address){
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE, false));
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, false));
}
}

View File

@ -21,6 +21,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
*/
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.Calendar;
import java.util.Collections;
@ -57,7 +58,7 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
}
public int normalizeType(int rawType) {
switch (rawType){
switch (rawType) {
case HPlusDataRecord.TYPE_DAY_SLOT:
case HPlusDataRecord.TYPE_DAY_SUMMARY:
case HPlusDataRecord.TYPE_REALTIME:
@ -141,6 +142,58 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
List<HPlusHealthActivityOverlay> overlayRecords = qb.build().list();
//Apply Overlays
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
//Create fake events to improve activity counters if there are no events around the overlay
//timestamp boundaries
//Insert one before, one at the beginning, one at the end, and one 1s after.
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom() - 1, timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo(), timestamp_to), overlay.getDeviceId(), overlay.getUserId());
}
Collections.sort(samples, new Comparator<HPlusHealthActivitySample>() {
public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) {
return one.getTimestamp() - other.getTimestamp();
}
});
//Apply Overlays
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
long nonSleepTimeEnd = 0;
for (HPlusHealthActivitySample sample : samples) {
if (sample.getRawKind() == ActivityKind.TYPE_NOT_WORN)
continue;
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
if (overlay.getRawKind() == ActivityKind.TYPE_NOT_WORN || overlay.getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP || overlay.getRawKind() == ActivityKind.TYPE_DEEP_SLEEP) {
if (sample.getRawKind() == HPlusDataRecord.TYPE_DAY_SLOT && sample.getSteps() > 0){
nonSleepTimeEnd = sample.getTimestamp() + 10 * 60; // 10 minutes
continue;
}else if(sample.getRawKind() == HPlusDataRecord.TYPE_REALTIME && sample.getTimestamp() <= nonSleepTimeEnd){
continue;
}
if (overlay.getRawKind() == ActivityKind.TYPE_NOT_WORN)
sample.setHeartRate(0);
if (sample.getRawKind() != ActivityKind.TYPE_NOT_WORN)
sample.setRawKind(overlay.getRawKind());
sample.setRawIntensity(10);
}
}
}
}
//Fix Step counters
//Todays sample steps will come from the Day Slots messages
//Historical steps will be provided by Day Summaries messages
//This will allow both week and current day results to be consistent
@ -154,8 +207,8 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
int stepsTodayCount = 0;
HPlusHealthActivitySample lastSample = null;
for(HPlusHealthActivitySample sample: samples){
if(sample.getTimestamp() >= today.getTimeInMillis() / 1000){
for (HPlusHealthActivitySample sample: samples) {
if (sample.getTimestamp() >= today.getTimeInMillis() / 1000) {
/**Strategy is:
* Calculate max steps from realtime messages
@ -170,7 +223,7 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
sample.setSteps(ActivitySample.NOT_MEASURED);
lastSample = sample;
}else{
} else {
if (sample.getRawKind() != HPlusDataRecord.TYPE_DAY_SUMMARY) {
sample.setSteps(ActivitySample.NOT_MEASURED);
}
@ -180,35 +233,8 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
if(lastSample != null)
lastSample.setSteps(Math.max(stepsTodayCount, stepsTodayMax));
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
//Create fake events to improve activity counters if there are no events around the overlay
//timestamp boundaries
//Insert one before, one at the beginning, one at the end, and one 1s after.
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom() - 1, timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo(), timestamp_to), overlay.getDeviceId(), overlay.getUserId());
for (HPlusHealthActivitySample sample : samples) {
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
if(overlay.getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP || overlay.getRawKind() == ActivityKind.TYPE_DEEP_SLEEP)
sample.setRawIntensity(10);
sample.setRawKind(overlay.getRawKind());
}
}
}
detachFromSession();
Collections.sort(samples, new Comparator<HPlusHealthActivitySample>() {
public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) {
return one.getTimestamp() - other.getTimestamp();
}
});
return samples;
}

View File

@ -99,12 +99,6 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public int getTapString() {
//TODO: changeme
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Sony Ericsson";
@ -125,6 +119,11 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet

View File

@ -82,7 +82,7 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
}
GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion()));
fwItem.setIcon(R.drawable.ic_device_miband);
fwItem.setIcon(device.getType().getIcon());
if (!helper.isFirmwareGenerallyCompatibleWith(device)) {
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2017 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
public enum DoNotDisturb {
OFF,
AUTOMATIC,
SCHEDULED
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Carsten Pfeiffer, José Rebelo
This file is part of Gadgetbridge.
@ -28,8 +28,12 @@ import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -117,6 +121,92 @@ public class MiBand2Coordinator extends MiBandCoordinator {
return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
}
public static Set<String> getDisplayItems() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getStringSet(MiBandConst.PREF_MI2_DISPLAY_ITEMS, null);
}
public static boolean getGoalNotification() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_GOAL_NOTIFICATION, false);
}
public static boolean getRotateWristToSwitchInfo() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO, false);
}
public static boolean getInactivityWarnings() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS, false);
}
public static int getInactivityWarningsThreshold() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getInt(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD, 60);
}
public static boolean getInactivityWarningsDnd() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND, false);
}
public static Date getInactivityWarningsStart() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_START, "06:00");
}
public static Date getInactivityWarningsEnd() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_END, "22:00");
}
public static Date getInactivityWarningsDndStart() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_START, "12:00");
}
public static Date getInactivityWarningsDndEnd() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_END, "14:00");
}
public static Date getDoNotDisturbStart() {
return getTimePreference(MiBandConst.PREF_MI2_DO_NOT_DISTURB_START, "01:00");
}
public static Date getDoNotDisturbEnd() {
return getTimePreference(MiBandConst.PREF_MI2_DO_NOT_DISTURB_END, "06:00");
}
public static Date getTimePreference(String key, String defaultValue) {
Prefs prefs = GBApplication.getPrefs();
String time = prefs.getString(key, defaultValue);
DateFormat df = new SimpleDateFormat("HH:mm");
try {
return df.parse(time);
} catch(Exception e) {
LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage());
}
return new Date();
}
public static DoNotDisturb getDoNotDisturb(Context context) {
Prefs prefs = GBApplication.getPrefs();
String dndOff = context.getString(R.string.p_off);
String dndAutomatic = context.getString(R.string.p_automatic);
String dndScheduled = context.getString(R.string.p_scheduled);
String pref = prefs.getString(MiBandConst.PREF_MI2_DO_NOT_DISTURB, dndOff);
if (dndAutomatic.equals(pref)) {
return DoNotDisturb.AUTOMATIC;
} else if (dndScheduled.equals(pref)) {
return DoNotDisturb.SCHEDULED;
}
return DoNotDisturb.OFF;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context);

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, JohnnySun, Uwe Hermann
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, JohnnySun,
José Rebelo, Uwe Hermann
This file is part of Gadgetbridge.
@ -53,7 +54,6 @@ public class MiBand2Service {
public static final int ALERT_LEVEL_MESSAGE = 1;
public static final int ALERT_LEVEL_PHONE_CALL = 2;
public static final int ALERT_LEVEL_VIBRATE_ONLY = 3;
public static final int ALERT_LEVEL_CUSTOM = 0xfa; // followed by another uin8 to select the actual icon
// set metric distance
// set 12 hour time mode
@ -145,6 +145,19 @@ public class MiBand2Service {
public static final byte ICON_HIGH_PRIORITY = 0x7;
public static byte ENDPOINT_DISPLAY_ITEMS = 0x0a;
public static byte DISPLAY_ITEM_BIT_CLOCK = 0x01;
public static byte DISPLAY_ITEM_BIT_STEPS = 0x02;
public static byte DISPLAY_ITEM_BIT_DISTANCE = 0x04;
public static byte DISPLAY_ITEM_BIT_CALORIES= 0x08;
public static byte DISPLAY_ITEM_BIT_HEART_RATE = 0x10;
public static byte DISPLAY_ITEM_BIT_BATTERY = 0x20;
// Second byte must be a bitwise OR combination of the above
// The clock can't be disabled
public static int SCREEN_CHANGE_BYTE = 1;
public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05};
public static byte ENDPOINT_DISPLAY = 0x06;
@ -154,9 +167,42 @@ public class MiBand2Service {
public static final byte[] DATEFORMAT_TIME_24_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x1 };
public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_GOAL_NOTIFICATION = new byte[]{ENDPOINT_DISPLAY, 0x06, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_GOAL_NOTIFICATION = new byte[]{ENDPOINT_DISPLAY, 0x06, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_ROTATE_WRIST_TO_SWITCH_INFO = new byte[]{ENDPOINT_DISPLAY, 0x0d, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_ROTATE_WRIST_TO_SWITCH_INFO = new byte[]{ENDPOINT_DISPLAY, 0x0d, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_DISPLAY_CALLER = new byte[]{ENDPOINT_DISPLAY, 0x10, 0x00, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_DISPLAY_CALLER = new byte[]{ENDPOINT_DISPLAY, 0x10, 0x00, 0x00, 0x00};
public static final byte[] DISPLAY_XXX = new byte[] {ENDPOINT_DISPLAY, 0x03, 0x0, 0x0 };
public static final byte[] DISPLAY_YYY = new byte[] {ENDPOINT_DISPLAY, 0x10, 0x0, 0x1, 0x1 };
// The third byte controls the threshold, in minutes
// The last 8 bytes represent 2 separate time intervals for the inactivity warnings
// If there is no do not disturb interval, the last 4 bytes (the second interval) are 0
// and only the first interval of the command is used
public static int INACTIVITY_WARNINGS_THRESHOLD = 2;
public static int INACTIVITY_WARNINGS_INTERVAL_1_START_HOURS = 4;
public static int INACTIVITY_WARNINGS_INTERVAL_1_START_MINUTES = 5;
public static int INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS = 6;
public static int INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES = 7;
public static int INACTIVITY_WARNINGS_INTERVAL_2_START_HOURS = 8;
public static int INACTIVITY_WARNINGS_INTERVAL_2_START_MINUTES = 9;
public static int INACTIVITY_WARNINGS_INTERVAL_2_END_HOURS = 10;
public static int INACTIVITY_WARNINGS_INTERVAL_2_END_MINUTES = 11;
public static final byte[] COMMAND_ENABLE_INACTIVITY_WARNINGS = new byte[] { 0x08, 0x01, 0x3c, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static final byte[] COMMAND_DISABLE_INACTIVITY_WARNINGS = new byte[] { 0x08, 0x00, 0x3c, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte ENDPOINT_DND = 0x09;
public static final byte[] COMMAND_DO_NOT_DISTURB_AUTOMATIC = new byte[] { ENDPOINT_DND, (byte) 0x83 };
public static final byte[] COMMAND_DO_NOT_DISTURB_OFF = new byte[] { ENDPOINT_DND, (byte) 0x82 };
public static final byte[] COMMAND_DO_NOT_DISTURB_SCHEDULED = new byte[] { ENDPOINT_DND, (byte) 0x81, 0x01, 0x00, 0x06, 0x00 };
// The 4 last bytes set the start and end time in 24h format
public static byte DND_BYTE_START_HOURS = 2;
public static byte DND_BYTE_START_MINUTES = 3;
public static byte DND_BYTE_END_HOURS = 4;
public static byte DND_BYTE_END_MINUTES = 5;
public static final byte RESPONSE = 0x10;
public static final byte SUCCESS = 0x01;

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, Szymon Tomasz Stefanek
Fischer, Daniele Gobbetti, José Rebelo, Szymon Tomasz Stefanek
This file is part of Gadgetbridge.
@ -35,8 +35,30 @@ public final class MiBandConst {
public static final String PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION = "mi_hr_sleep_detection";
public static final String PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS = "mi_device_time_offset_hours";
public static final String PREF_MI2_DATEFORMAT = "mi2_dateformat";
public static final String PREF_MI2_GOAL_NOTIFICATION = "mi2_goal_notification";
public static final String PREF_MI2_DISPLAY_ITEMS = "mi2_display_items";
public static final String PREF_MI2_DISPLAY_ITEM_CLOCK = "clock";
public static final String PREF_MI2_DISPLAY_ITEM_STEPS = "steps";
public static final String PREF_MI2_DISPLAY_ITEM_DISTANCE = "distance";
public static final String PREF_MI2_DISPLAY_ITEM_CALORIES = "calories";
public static final String PREF_MI2_DISPLAY_ITEM_HEART_RATE = "heart_rate";
public static final String PREF_MI2_DISPLAY_ITEM_BATTERY = "battery";
public static final String PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT = "mi2_activate_display_on_lift_wrist";
public static final String PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO = "mi2_rotate_wrist_to_switch_info";
public static final String PREF_MI2_ENABLE_TEXT_NOTIFICATIONS = "mi2_enable_text_notifications";
public static final String PREF_MI2_DO_NOT_DISTURB = "mi2_do_not_disturb";
public static final String PREF_MI2_DO_NOT_DISTURB_OFF = "off";
public static final String PREF_MI2_DO_NOT_DISTURB_AUTOMATIC = "automatic";
public static final String PREF_MI2_DO_NOT_DISTURB_SCHEDULED = "scheduled";
public static final String PREF_MI2_DO_NOT_DISTURB_START = "mi2_do_not_disturb_start";
public static final String PREF_MI2_DO_NOT_DISTURB_END = "mi2_do_not_disturb_end";
public static final String PREF_MI2_INACTIVITY_WARNINGS = "mi2_inactivity_warnings";
public static final String PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD = "mi2_inactivity_warnings_threshold";
public static final String PREF_MI2_INACTIVITY_WARNINGS_START = "mi2_inactivity_warnings_start";
public static final String PREF_MI2_INACTIVITY_WARNINGS_END = "mi2_inactivity_warnings_end";
public static final String PREF_MI2_INACTIVITY_WARNINGS_DND = "mi2_inactivity_warnings_dnd";
public static final String PREF_MI2_INACTIVITY_WARNINGS_DND_START = "mi2_inactivity_warnings_dnd_start";
public static final String PREF_MI2_INACTIVITY_WARNINGS_DND_END = "mi2_inactivity_warnings_dnd_end";
public static final String PREF_MIBAND_SETUP_BT_PAIRING = "mi_setup_bt_pairing";

View File

@ -151,11 +151,6 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Xiaomi";
@ -176,6 +171,11 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
public static boolean hasValidUserInfo() {
String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00";
try {

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, Szymon Tomasz Stefanek
Fischer, Daniele Gobbetti, José Rebelo, Szymon Tomasz Stefanek
This file is part of Gadgetbridge.
@ -34,12 +34,28 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_ALARM_CLOCK;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_INCOMING_CALL;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DISPLAY_ITEMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_SCHEDULED;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ENABLE_TEXT_NOTIFICATIONS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_GOAL_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ADDRESS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR;
@ -57,6 +73,8 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
addTryListeners();
Prefs prefs = GBApplication.getPrefs();
final Preference enableHeartrateSleepSupport = findPreference(PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION);
enableHeartrateSleepSupport.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -66,6 +84,20 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
}
});
final Preference goalNotification = findPreference(PREF_MI2_GOAL_NOTIFICATION);
goalNotification.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_GOAL_NOTIFICATION);
}
});
return true;
}
});
final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT);
setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -80,6 +112,20 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
}
});
final Preference displayPages = findPreference(PREF_MI2_DISPLAY_ITEMS);
displayPages.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS);
}
});
return true;
}
});
final Preference activateDisplayOnLift = findPreference(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
activateDisplayOnLift.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -94,6 +140,170 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
}
});
final Preference rotateWristCycleInfo = findPreference(PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO);
rotateWristCycleInfo.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO);
}
});
return true;
}
});
final Preference inactivityWarnings = findPreference(PREF_MI2_INACTIVITY_WARNINGS);
inactivityWarnings.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS);
}
});
return true;
}
});
final Preference inactivityWarningsThreshold = findPreference(PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD);
inactivityWarningsThreshold.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD);
}
});
return true;
}
});
final Preference inactivityWarningsStart = findPreference(PREF_MI2_INACTIVITY_WARNINGS_START);
inactivityWarningsStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_START);
}
});
return true;
}
});
final Preference inactivityWarningsEnd = findPreference(PREF_MI2_INACTIVITY_WARNINGS_END);
inactivityWarningsEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_END);
}
});
return true;
}
});
final Preference inactivityWarningsDnd = findPreference(PREF_MI2_INACTIVITY_WARNINGS_DND);
inactivityWarningsDnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_DND);
}
});
return true;
}
});
final Preference inactivityWarningsDndStart = findPreference(PREF_MI2_INACTIVITY_WARNINGS_DND_START);
inactivityWarningsDndStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_DND_START);
}
});
return true;
}
});
final Preference inactivityWarningsDndEnd = findPreference(PREF_MI2_INACTIVITY_WARNINGS_DND_END);
inactivityWarningsDndEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_DND_END);
}
});
return true;
}
});
String doNotDisturbState = prefs.getString(MiBandConst.PREF_MI2_DO_NOT_DISTURB, PREF_MI2_DO_NOT_DISTURB_OFF);
boolean doNotDisturbScheduled = doNotDisturbState.equals(PREF_MI2_DO_NOT_DISTURB_SCHEDULED);
final Preference doNotDisturbStart = findPreference(PREF_MI2_DO_NOT_DISTURB_START);
doNotDisturbStart.setEnabled(doNotDisturbScheduled);
doNotDisturbStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DO_NOT_DISTURB_START);
}
});
return true;
}
});
final Preference doNotDisturbEnd = findPreference(PREF_MI2_DO_NOT_DISTURB_END);
doNotDisturbEnd.setEnabled(doNotDisturbScheduled);
doNotDisturbEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DO_NOT_DISTURB_END);
}
});
return true;
}
});
final Preference doNotDisturb = findPreference(PREF_MI2_DO_NOT_DISTURB);
doNotDisturb.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
final boolean scheduled = PREF_MI2_DO_NOT_DISTURB_SCHEDULED.equals(newVal.toString());
doNotDisturbStart.setEnabled(scheduled);
doNotDisturbEnd.setEnabled(scheduled);
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DO_NOT_DISTURB);
}
});
return true;
}
});
final Preference fitnessGoal = findPreference(ActivityUser.PREF_USER_STEPS_GOAL);
fitnessGoal.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -165,6 +375,7 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
prefKeys.add(PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR);
prefKeys.add(PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
prefKeys.add(PREF_MI2_ENABLE_TEXT_NOTIFICATIONS);
prefKeys.add(PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD);
prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_ALARM_CLOCK));
prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL));

View File

@ -137,11 +137,6 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return PebbleUtils.hasHRM(device.getModel());
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_app_mananger;
}
@Override
public String getManufacturer() {
return "Pebble";
@ -161,4 +156,9 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
public boolean supportsCalendarEvents() {
return true;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
}

View File

@ -37,11 +37,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class PebbleHealthSampleProvider extends AbstractSampleProvider<PebbleHealthActivitySample> {
public static final int TYPE_LIGHT_SLEEP = 1;
public static final int TYPE_DEEP_SLEEP = 2;
public static final int TYPE_LIGHT_NAP = 3; //probably
public static final int TYPE_DEEP_NAP = 4; //probably
public static final int TYPE_WALK = 5; //probably
public static final int TYPE_LIGHT_NAP = 3;
public static final int TYPE_DEEP_NAP = 4;
public static final int TYPE_WALK = 5;
public static final int TYPE_RUN = 6;
public static final int TYPE_ACTIVITY = -1;
protected final float movementDivisor = 8000f;
public PebbleHealthSampleProvider(GBDevice device, DaoSession session) {
@ -114,6 +116,8 @@ public class PebbleHealthSampleProvider extends AbstractSampleProvider<PebbleHea
case TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
case TYPE_ACTIVITY:
case TYPE_WALK:
case TYPE_RUN:
return ActivityKind.TYPE_ACTIVITY;
default:
return ActivityKind.TYPE_UNKNOWN;

View File

@ -100,11 +100,6 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_vibration;
}
@Override
public String getManufacturer() {
return "Amor AG";
@ -125,6 +120,11 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false; // hmmm well, it has a temperature sensor :D
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet

View File

@ -34,7 +34,6 @@ import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncState;

View File

@ -48,6 +48,12 @@ public class MusicPlaybackReceiver extends BroadcastReceiver {
MusicStateSpec stateSpec = new MusicStateSpec(lastStateSpec);
Bundle incomingBundle = intent.getExtras();
if (incomingBundle == null) {
LOG.warn("Not processing incoming null bundle.");
return;
}
for (String key : incomingBundle.keySet()) {
Object incoming = incomingBundle.get(key);
if (incoming instanceof String && "artist".equals(key)) {

View File

@ -18,7 +18,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
@ -30,17 +29,19 @@ import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -76,7 +77,7 @@ public class NotificationListener extends NotificationListenerService {
private LimitedQueue mActionLookup = new LimitedQueue(16);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("NewApi")
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
@ -102,7 +103,7 @@ public class NotificationListener extends NotificationListenerService {
} else {
// ACTION_MUTE
LOG.info("going to mute " + sbn.getPackageName());
GBApplication.addToBlacklist(sbn.getPackageName());
GBApplication.addAppToBlacklist(sbn.getPackageName());
}
}
}
@ -177,16 +178,8 @@ public class NotificationListener extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
/*
* return early if DeviceCommunicationService is not running,
* else the service would get started every time we get a notification.
* unfortunately we cannot enable/disable NotificationListener at runtime like we do with
* broadcast receivers because it seems to invalidate the permissions that are
* necessary for NotificationListenerService
*/
if (!isServiceRunning()) {
if (shouldIgnore(sbn))
return;
}
switch (GBApplication.getGrantedInterruptionFilter()) {
case NotificationManager.INTERRUPTION_FILTER_ALL:
@ -201,53 +194,8 @@ public class NotificationListener extends NotificationListenerService {
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if (handleMediaSessionNotification(notification))
return;
Prefs prefs = GBApplication.getPrefs();
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
if (powermanager.isScreenOn()) {
// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
return;
}
}
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags);
return;
}
/* do not display messages from "android"
* This includes keyboard selection message, usb connection messages, etc
* Hope it does not filter out too much, we will see...
*/
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
LOG.info("Not forwarding notification, is a system event");
return;
}
if (source.equals("com.moez.QKSMS") ||
source.equals("com.android.mms") ||
source.equals("com.sonyericsson.conversations") ||
source.equals("com.android.messaging") ||
source.equals("org.smssecure.smssecure")) {
if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) {
return;
}
}
if (GBApplication.isBlacklisted(source)) {
LOG.info("Not forwarding notification, application is blacklisted");
return;
}
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
// determinate Source App Name ("Label")
PackageManager pm = getPackageManager();
@ -266,10 +214,6 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = AppNotificationType.getInstance().get(source);
if (source.startsWith("com.fsck.k9")) {
// we dont want group summaries at all for k9
if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) {
return;
}
preferBigText = true;
}
@ -277,10 +221,9 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = NotificationType.UNKNOWN;
}
LOG.info("Processing notification from source " + source + " with flags: " + notification.flags);
LOG.info("Processing notification " + notificationSpec.id + " from source " + source + " with flags: " + notification.flags);
dissectNotificationTo(notification, notificationSpec, preferBigText);
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
// ignore Gadgetbridge's very own notifications, except for those from the debug screen
if (getApplicationContext().getPackageName().equals(source)) {
@ -292,11 +235,6 @@ public class NotificationListener extends NotificationListenerService {
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
List<NotificationCompat.Action> actions = wearableExtender.getActions();
if (actions.isEmpty() && (notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) { //this could cause #395 to come back
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags);
return;
}
for (NotificationCompat.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) {
LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag());
@ -306,11 +244,17 @@ public class NotificationListener extends NotificationListenerService {
}
}
if ((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) == 0 && NotificationCompat.isGroupSummary(notification)) { //this could cause #395 to come back
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags);
return;
}
GBApplication.deviceService().onNotification(notificationSpec);
}
private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, boolean preferBigText) {
Bundle extras = notification.extras;
Bundle extras = NotificationCompat.getExtras(notification);
//dumpExtras(extras);
@ -321,9 +265,9 @@ public class NotificationListener extends NotificationListenerService {
CharSequence contentCS = null;
if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) {
contentCS = extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
contentCS = extras.getCharSequence(NotificationCompat.EXTRA_BIG_TEXT);
} else if (extras.containsKey(Notification.EXTRA_TEXT)) {
contentCS = extras.getCharSequence(Notification.EXTRA_TEXT);
contentCS = extras.getCharSequence(NotificationCompat.EXTRA_TEXT);
}
if (contentCS != null) {
notificationSpec.body = contentCS.toString();
@ -344,31 +288,18 @@ public class NotificationListener extends NotificationListenerService {
/**
* Try to handle media session notifications that tell info about the current play state.
*
* @param notification The notification to handle.
* @param mediaSession The mediasession to handle.
* @return true if notification was handled, false otherwise
*/
public boolean handleMediaSessionNotification(Notification notification) {
// this code requires Android 5.0 or newer
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}
public boolean handleMediaSessionNotification(MediaSessionCompat.Token mediaSession) {
MusicSpec musicSpec = new MusicSpec();
MusicStateSpec stateSpec = new MusicStateSpec();
Bundle extras = notification.extras;
if (extras == null)
return false;
if (extras.get(Notification.EXTRA_MEDIA_SESSION) == null)
return false;
MediaController c;
MediaControllerCompat c;
try {
c = new MediaController(getApplicationContext(), (MediaSession.Token) extras.get(Notification.EXTRA_MEDIA_SESSION));
c = new MediaControllerCompat(getApplicationContext(), mediaSession);
PlaybackState s = c.getPlaybackState();
PlaybackStateCompat s = c.getPlaybackState();
stateSpec.position = (int) (s.getPosition() / 1000);
stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed());
stateSpec.repeat = 1;
@ -388,57 +319,41 @@ public class NotificationListener extends NotificationListenerService {
break;
}
MediaMetadata d = c.getMetadata();
MediaMetadataCompat d = c.getMetadata();
if (d == null)
return false;
if (d.containsKey(MediaMetadata.METADATA_KEY_ARTIST))
musicSpec.artist = d.getString(MediaMetadata.METADATA_KEY_ARTIST);
musicSpec.artist = d.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
if (d.containsKey(MediaMetadata.METADATA_KEY_ALBUM))
musicSpec.album = d.getString(MediaMetadata.METADATA_KEY_ALBUM);
musicSpec.album = d.getString(MediaMetadataCompat.METADATA_KEY_ALBUM);
if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE))
musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE);
musicSpec.track = d.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION))
musicSpec.duration = (int) d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000;
musicSpec.duration = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_DURATION) / 1000;
if (d.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS))
musicSpec.trackCount = (int) d.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
musicSpec.trackCount = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS);
if (d.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER))
musicSpec.trackNr = (int) d.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
musicSpec.trackNr = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER);
// finally, tell the device about it
GBApplication.deviceService().onSetMusicInfo(musicSpec);
GBApplication.deviceService().onSetMusicState(stateSpec);
return true;
} catch (NullPointerException e) {
} catch (NullPointerException | RemoteException e) {
return false;
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
//FIXME: deduplicate code
if (!isServiceRunning() || sbn == null) {
if (shouldIgnore(sbn))
return;
}
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
return;
}
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
return;
}
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("autoremove_notifications", false)) {
LOG.info("notification removed, will ask device to delete it");
GBApplication.deviceService().onDeleteNotification((int) sbn.getPostTime()); //FIMXE: a truly unique id would be better
GBApplication.deviceService().onDeleteNotification(sbn.getPackageName().hashCode() * 31 + sbn.getId());
}
}
@ -451,4 +366,88 @@ public class NotificationListener extends NotificationListenerService {
LOG.debug(String.format("Notification extra: %s %s (%s)", key, value.toString(), value.getClass().getName()));
}
}
private boolean shouldIgnore(StatusBarNotification sbn) {
/*
* return early if DeviceCommunicationService is not running,
* else the service would get started every time we get a notification.
* unfortunately we cannot enable/disable NotificationListener at runtime like we do with
* broadcast receivers because it seems to invalidate the permissions that are
* necessary for NotificationListenerService
*/
if (!isServiceRunning() || sbn == null) {
return true;
}
if (shouldIgnoreSource(sbn.getPackageName()))
return true;
if (shouldIgnoreNotification(sbn.getNotification()))
return true;
return false;
}
private boolean shouldIgnoreSource(String source) {
Prefs prefs = GBApplication.getPrefs();
/* do not display messages from "android"
* This includes keyboard selection message, usb connection messages, etc
* Hope it does not filter out too much, we will see...
*/
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
LOG.info("Ignoring notification, is a system event");
return true;
}
if (source.equals("com.moez.QKSMS") ||
source.equals("com.android.mms") ||
source.equals("com.sonyericsson.conversations") ||
source.equals("com.android.messaging") ||
source.equals("org.smssecure.smssecure")) {
if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) {
return true;
}
}
if (GBApplication.appIsBlacklisted(source)) {
LOG.info("Ignoring notification, application is blacklisted");
return true;
}
return false;
}
private boolean shouldIgnoreNotification(Notification notification) {
MediaSessionCompat.Token mediaSession = NotificationCompat.getMediaSession(notification);
//try to handle media session notifications
if (mediaSession != null && handleMediaSessionNotification(mediaSession))
return true;
//ignore notifications marked as LocalOnly https://developer.android.com/reference/android/app/Notification.html#FLAG_LOCAL_ONLY
if (NotificationCompat.getLocalOnly(notification))
return true;
Prefs prefs = GBApplication.getPrefs();
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
if (powermanager.isScreenOn()) {
// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
return true;
}
}
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags);
return true;
}
return false;
}
}

View File

@ -23,7 +23,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract.Instances;
import android.text.format.Time;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,6 +33,8 @@ import java.util.GregorianCalendar;
import java.util.List;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class CalendarEvents {
private static final Logger LOG = LoggerFactory.getLogger(CalendarEvents.class);
@ -102,7 +103,11 @@ public class CalendarEvents {
evtCursor.getString(7),
!evtCursor.getString(8).equals("0")
);
calendarEventList.add(calEvent);
if (!GBApplication.calendarIsBlacklisted(calEvent.getCalName())) {
calendarEventList.add(calEvent);
} else {
LOG.debug("calendar " + calEvent.getCalName() + " skipped because it's blacklisted");
}
}
return true;
}

View File

@ -28,15 +28,16 @@ import nodomain.freeyourgadget.gadgetbridge.R;
* and may not be changed.
*/
public enum DeviceType {
UNKNOWN(-1, R.drawable.ic_launcher, R.drawable.ic_device_default_disabled),
UNKNOWN(-1, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled),
PEBBLE(1, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled),
MIBAND(10, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled),
MIBAND2(11, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled),
AMAZFITBIP(12, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled),
VIBRATISSIMO(20, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled),
LIVEVIEW(30, R.drawable.ic_launcher, R.drawable.ic_device_default_disabled),
LIVEVIEW(30, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled),
HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled),
MAKIBESF68(41, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled),
TEST(1000, R.drawable.ic_launcher, R.drawable.ic_device_default_disabled);
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled);
private final int key;
@DrawableRes

View File

@ -18,6 +18,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.Service;
@ -27,9 +28,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
@ -584,12 +587,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
if (enable && initialized && coordinator != null && coordinator.supportsCalendarEvents()) {
if (mCalendarReceiver == null && getPrefs().getBoolean("enable_calendar_sync", true)) {
IntentFilter calendarIntentFilter = new IntentFilter();
calendarIntentFilter.addAction("android.intent.action.PROVIDER_CHANGED");
calendarIntentFilter.addDataScheme("content");
calendarIntentFilter.addDataAuthority("com.android.calendar", null);
mCalendarReceiver = new CalendarReceiver(mGBDevice);
registerReceiver(mCalendarReceiver, calendarIntentFilter);
if (!(GBApplication.isRunningMarshmallowOrLater() && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)) {
IntentFilter calendarIntentFilter = new IntentFilter();
calendarIntentFilter.addAction("android.intent.action.PROVIDER_CHANGED");
calendarIntentFilter.addDataScheme("content");
calendarIntentFilter.addDataAuthority("com.android.calendar", null);
mCalendarReceiver = new CalendarReceiver(mGBDevice);
registerReceiver(mCalendarReceiver, calendarIntentFilter);
}
}
if (mAlarmReceiver == null) {
mAlarmReceiver = new AlarmReceiver();

View File

@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
@ -109,6 +110,9 @@ public class DeviceSupportFactory {
case MIBAND2:
deviceSupport = new ServiceDeviceSupport(new MiBand2Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case AMAZFITBIP:
deviceSupport = new ServiceDeviceSupport(new AmazfitBipSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case VIBRATISSIMO:
deviceSupport = new ServiceDeviceSupport(new VibratissimoSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;

View File

@ -0,0 +1,202 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btclassic;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.ParcelUuid;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class BtClassicIoThread extends GBDeviceIoThread {
private static final Logger LOG = LoggerFactory.getLogger(BtClassicIoThread.class);
private final GBDeviceProtocol mProtocol;
private final AbstractSerialDeviceSupport mDeviceSupport;
private BluetoothAdapter mBtAdapter = null;
private BluetoothSocket mBtSocket = null;
private InputStream mInStream = null;
private OutputStream mOutStream = null;
private boolean mQuit = false;
@Override
public void quit() {
mQuit = true;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
}
}
private boolean mIsConnected = false;
public BtClassicIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol deviceProtocol, AbstractSerialDeviceSupport deviceSupport, BluetoothAdapter btAdapter) {
super(gbDevice, context);
mProtocol = deviceProtocol;
mDeviceSupport = deviceSupport;
mBtAdapter = btAdapter;
}
@Override
public synchronized void write(byte[] bytes) {
if (null == bytes)
return;
LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length));
try {
mOutStream.write(bytes);
mOutStream.flush();
} catch (IOException e) {
LOG.error("Error writing.", e);
}
}
@Override
public void run() {
mIsConnected = connect();
if (!mIsConnected) {
setUpdateState(GBDevice.State.NOT_CONNECTED);
return;
}
mQuit = false;
while (!mQuit) {
LOG.info("Ready for a new message exchange.");
try {
GBDeviceEvent deviceEvents[] = mProtocol.decodeResponse(parseIncoming(mInStream));
if (deviceEvents == null) {
LOG.info("unhandled message");
} else {
for (GBDeviceEvent deviceEvent : deviceEvents) {
if (deviceEvent == null) {
continue;
}
mDeviceSupport.evaluateGBDeviceEvent(deviceEvent);
}
}
} catch (SocketTimeoutException ignore) {
LOG.debug("socket timeout, we can't help but ignore this");
} catch (IOException e) {
LOG.info(e.getMessage());
mIsConnected = false;
mBtSocket = null;
mInStream = null;
mOutStream = null;
LOG.info("Bluetooth socket closed, will quit IO Thread");
break;
}
}
mIsConnected = false;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
mBtSocket = null;
}
setUpdateState(GBDevice.State.NOT_CONNECTED);
}
@Override
protected boolean connect() {
GBDevice.State originalState = gbDevice.getState();
setUpdateState(GBDevice.State.CONNECTING);
try {
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress());
ParcelUuid uuids[] = btDevice.getUuids();
if (uuids == null) {
LOG.warn("Device provided no UUIDs to connect to, giving up: " + gbDevice);
return false;
}
for (ParcelUuid uuid : uuids) {
LOG.info("found service UUID " + uuid);
}
mBtSocket = btDevice.createRfcommSocketToServiceRecord(getUuidToConnect(uuids));
mBtSocket.connect();
mInStream = mBtSocket.getInputStream();
mOutStream = mBtSocket.getOutputStream();
setUpdateState(GBDevice.State.CONNECTED);
} catch (IOException e) {
LOG.error("Server socket cannot be started.");
//LOG.error(e.getMessage());
setUpdateState(originalState);
mInStream = null;
mOutStream = null;
mBtSocket = null;
return false;
}
write(mProtocol.encodeSetTime());
setUpdateState(GBDevice.State.INITIALIZED);
return true;
}
/**
* Returns the uuid to connect to.
* Default implementation returns the first of the given uuids that were
* read from the remote device.
* @param uuids
* @return
*/
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return uuids[0].getUuid();
}
protected void setUpdateState(GBDevice.State state) {
gbDevice.setState(state);
gbDevice.sendDeviceUpdateIntent(getContext());
}
/**
* Returns an incoming message for consuming by the GBDeviceProtocol
* @return
* @throws IOException
* @param inStream
*/
protected abstract byte[] parseIncoming(InputStream inStream) throws IOException;
}

View File

@ -1,3 +1,19 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
/*
* Copyright (C) 2013 The Android Open Source Project
*

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Uwe Hermann
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Uwe Hermann
This file is part of Gadgetbridge.
@ -37,7 +37,8 @@ public enum AlertCategory {
// 10-250 reserved for future use
// 251-255 defined by service specification
Any(255),
Custom(-1);
Custom(-1),
CustomMiBand2(-6);
private final int id;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -33,12 +33,16 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> extends AbstractBleProfile<T> {
private static final Logger LOG = LoggerFactory.getLogger(AlertNotificationProfile.class);
private static final int MAX_MSG_LENGTH = 18;
private int maxLength = 18; // Mi2-ism?
public AlertNotificationProfile(T support) {
super(support);
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
public void configure(TransactionBuilder builder, AlertNotificationControl control) {
BluetoothGattCharacteristic characteristic = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_NOTIFICATION_CONTROL_POINT);
if (characteristic != null) {
@ -57,21 +61,21 @@ public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> exten
BluetoothGattCharacteristic characteristic = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_NEW_ALERT);
if (characteristic != null) {
String message = StringUtils.ensureNotNull(alert.getMessage());
if (message.length() > MAX_MSG_LENGTH && strategy == OverflowStrategy.TRUNCATE) {
message = StringUtils.truncate(message, MAX_MSG_LENGTH);
if (message.length() > maxLength && strategy == OverflowStrategy.TRUNCATE) {
message = StringUtils.truncate(message, maxLength);
}
int numChunks = message.length() / MAX_MSG_LENGTH;
if (message.length() % MAX_MSG_LENGTH > 0) {
int numChunks = message.length() / maxLength;
if (message.length() % maxLength > 0) {
numChunks++;
}
try {
boolean hasAlerted = false;
for (int i = 0; i < numChunks; i++) {
int offset = i * MAX_MSG_LENGTH;
int offset = i * maxLength;
int restLength = message.length() - offset;
message = message.substring(offset, offset + Math.min(MAX_MSG_LENGTH, restLength));
message = message.substring(offset, offset + Math.min(maxLength, restLength));
if (hasAlerted && message.length() == 0) {
// no need to do it again when there is no text content
break;
@ -91,10 +95,17 @@ public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> exten
}
}
public void newAlert(TransactionBuilder builder, NewAlert alert) {
newAlert(builder, alert, OverflowStrategy.TRUNCATE);
}
protected byte[] getAlertMessage(NewAlert alert, String message, int chunk) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream(100);
stream.write(BLETypeConversions.fromUint8(alert.getCategory().getId()));
stream.write(BLETypeConversions.fromUint8(alert.getNumAlerts()));
if (alert.getCategory() == AlertCategory.CustomMiBand2) {
stream.write(BLETypeConversions.fromUint8(alert.getCustomIcon()));
}
if (message.length() > 0) {
stream.write(BLETypeConversions.toUtf8s(message));

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -16,6 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
import android.icu.util.IslamicCalendar;
/**
* https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.new_alert.xml&u=org.bluetooth.characteristic.new_alert.xml
*
@ -47,6 +49,7 @@ public class NewAlert {
private final AlertCategory category;
private final int numAlerts;
private final String message;
private int customIcon = -1;
public NewAlert(AlertCategory category, int /*uint8*/ numAlerts, String /*utf8s*/ message) {
this.category = category;
@ -54,6 +57,13 @@ public class NewAlert {
this.message = message;
}
public NewAlert(AlertCategory category, int /*uint8*/ numAlerts, String /*utf8s*/ message, int customIcon) {
this.category = category;
this.numAlerts = numAlerts;
this.message = message;
this.customIcon = customIcon;
}
public AlertCategory getCategory() {
return category;
}
@ -65,4 +75,8 @@ public class NewAlert {
public String getMessage() {
return message;
}
public int getCustomIcon() {
return customIcon;
}
}

View File

@ -0,0 +1,80 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.FirmwareType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
public class AmazfitBipFirmwareInfo extends Mi2FirmwareInfo {
// total crap maybe
private static final byte[] GPS_HEADER = new byte[]{
(byte) 0xcb, 0x51, (byte) 0xc1, 0x30, 0x41, (byte) 0x9e, 0x5e, (byte) 0xd3,
0x51, 0x35, (byte) 0xdf, 0x66, (byte) 0xed, (byte) 0xd9, 0x5f, (byte) 0xa7
};
// guessed - at least it is the same accross current versions and different from other devices
private static final byte[] FW_HEADER = new byte[]{
0x68, 0x46, 0x70, 0x47, 0x68, 0x46, 0x70, 0x47,
0x68, 0x46, 0x70, 0x47, 0x68, 0x46, 0x70, 0x47
};
private static final int FW_HEADER_OFFSET = 0x9330;
private static final byte[] RES_HEADER = new byte[]{ // HMRES resources file (*.res)
0x48, 0x4d, 0x52, 0x45, 0x53
};
static {
// firmware
crcToVersion.put(25257, "0.0.8.74");
// resources
crcToVersion.put(12586, "RES 0.0.8.74");
// gps
crcToVersion.put(61520, "GPS 0.0.8.xx");
}
public AmazfitBipFirmwareInfo(byte[] bytes) {
super(bytes);
}
@Override
protected FirmwareType determineFirmwareType(byte[] bytes) {
if (ArrayUtils.startsWith(bytes, RES_HEADER)) {
return FirmwareType.RES;
}
if (ArrayUtils.startsWith(bytes, GPS_HEADER)) {
return FirmwareType.GPS;
}
if (ArrayUtils.equals(bytes, FW_HEADER, FW_HEADER_OFFSET)) {
// TODO: this is certainly not a correct validation, but it works for now
return FirmwareType.FIRMWARE;
}
return FirmwareType.INVALID;
}
@Override
public boolean isGenerallyCompatibleWith(GBDevice device) {
return isHeaderValid() && device.getType() == DeviceType.AMAZFITBIP;
}
}

View File

@ -0,0 +1,184 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
import android.net.Uri;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.SimpleTimeZone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipIcon;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipService;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipWeatherConditions;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.operations.AmazfitBipUpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class AmazfitBipSupport extends MiBand2Support {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipSupport.class);
@Override
public NotificationStrategy getNotificationStrategy() {
return new AmazfitBipTextNotificationStrategy(this);
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) {
onAlarmClock(notificationSpec);
return;
}
String senderOrTiltle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
String message = StringUtils.truncate(senderOrTiltle, 32) + "\0";
if (notificationSpec.subject != null) {
message += StringUtils.truncate(notificationSpec.subject, 128) + "\n\n";
}
if (notificationSpec.body != null) {
message += StringUtils.truncate(notificationSpec.body, 128);
}
try {
TransactionBuilder builder = performInitialized("new notification");
AlertNotificationProfile<?> profile = new AlertNotificationProfile(this);
profile.setMaxLength(255); // TODO: find out real limit, certainly it is more than 18 which is default
int customIconId = AmazfitBipIcon.mapToIconId(notificationSpec.type);
AlertCategory alertCategory = AlertCategory.CustomMiBand2;
// The SMS icon for AlertCategory.SMS is unique and not available as iconId
if (notificationSpec.type == NotificationType.GENERIC_SMS) {
alertCategory = AlertCategory.SMS;
}
NewAlert alert = new NewAlert(alertCategory, 1, message, customIconId);
profile.newAlert(builder, alert);
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Unable to send notification to Amazfit Bip", ex);
}
}
@Override
public void onFindDevice(boolean start) {
CallSpec callSpec = new CallSpec();
callSpec.command = start ? CallSpec.CALL_INCOMING : CallSpec.CALL_END;
callSpec.name = "Gadgetbridge";
onSetCallState(callSpec);
}
@Override
public void handleButtonPressed(byte[] value) {
if (value == null || value.length != 1) {
return;
}
GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl();
if (value[0] == 0x07) {
callCmd.event = GBDeviceEventCallControl.Event.REJECT;
} else if (value[0] == 0x09) {
callCmd.event = GBDeviceEventCallControl.Event.ACCEPT;
} else {
return;
}
evaluateGBDeviceEvent(callCmd);
}
@Override
public void onInstallApp(Uri uri) {
try {
new AmazfitBipUpdateFirmwareOperation(uri, this).perform();
} catch (IOException ex) {
GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
try {
TransactionBuilder builder = performInitialized("Sending weather forecast");
Version version = new Version(gbDevice.getFirmwareVersion());
boolean supportsConditionString = false;
if (version.compareTo(new Version("0.0.8.74")) >= 0) {
supportsConditionString = true;
}
final byte NR_DAYS = 2;
int bytesPerDay = 4;
int conditionsLength = 0;
if (supportsConditionString) {
bytesPerDay = 5;
conditionsLength = weatherSpec.currentCondition.getBytes().length;
}
int length = 7 + bytesPerDay * NR_DAYS + conditionsLength;
ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 1);
buf.putInt(weatherSpec.timestamp);
int tz_offset_hours = SimpleTimeZone.getDefault().getOffset(weatherSpec.timestamp * 1000L) / (1000 * 60 * 60);
buf.put((byte) (tz_offset_hours * 4));
buf.put(NR_DAYS);
byte condition = AmazfitBipWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
buf.put(condition);
buf.put(condition);
buf.put((byte) (weatherSpec.todayMaxTemp - 273));
buf.put((byte) (weatherSpec.todayMinTemp - 273));
if (supportsConditionString) {
buf.put(weatherSpec.currentCondition.getBytes());
buf.put((byte) 0); //
}
condition = AmazfitBipWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.tomorrowConditionCode);
buf.put(condition);
buf.put(condition);
buf.put((byte) (weatherSpec.tomorrowMaxTemp - 273));
buf.put((byte) (weatherSpec.tomorrowMinTemp - 273));
if (supportsConditionString) {
buf.put((byte) 0); // not yet in weatherspec
}
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
builder.queue(getQueue());
} catch (IOException ignore) {
}
}
}

View File

@ -0,0 +1,67 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
import android.support.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.OverflowStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2TextNotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
// This class in no longer in use except for incoming calls
class AmazfitBipTextNotificationStrategy extends Mi2TextNotificationStrategy {
AmazfitBipTextNotificationStrategy(MiBand2Support support) {
super(support);
}
@Override
protected void sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, BtLEAction extraAction, TransactionBuilder builder) {
if (simpleNotification != null && !StringUtils.isEmpty(simpleNotification.getMessage())) {
sendAlert(simpleNotification, builder);
}
}
@Override
protected void sendAlert(@NonNull SimpleNotification simpleNotification, TransactionBuilder builder) {
AlertNotificationProfile<?> profile = new AlertNotificationProfile<>(getSupport());
profile.setMaxLength(255); // TODO: find out real limit, certainly it is more than 18 which is default
AlertCategory category = simpleNotification.getAlertCategory();
switch (simpleNotification.getAlertCategory()) {
// only these are confirmed working so far on Amazfit Bip
case Email:
case IncomingCall:
case SMS:
break;
// default to SMS for non working categories
default:
category = AlertCategory.SMS;
}
NewAlert alert = new NewAlert(category, 1, simpleNotification.getMessage());
profile.newAlert(builder, alert, OverflowStrategy.TRUNCATE);
}
}

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.operations;
import android.net.Uri;
import android.widget.Toast;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AmazfitBipUpdateFirmwareOperation extends UpdateFirmwareOperation {
public AmazfitBipUpdateFirmwareOperation(Uri uri, AmazfitBipSupport support) {
super(uri, support);
}
@Override
protected void doPerform() throws IOException {
AmazfitBipFWHelper mFwHelper = new AmazfitBipFWHelper(uri, getContext());
firmwareInfo = mFwHelper.getFirmwareInfo();
if (!firmwareInfo.isGenerallyCompatibleWith(getDevice())) {
throw new IOException("Firmware is not compatible with the given device: " + getDevice().getAddress());
}
if (!sendFwInfo()) {
displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR);
done();
}
//the firmware will be sent by the notification listener if the band confirms that the metadata are ok.
}
}

View File

@ -39,23 +39,23 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
/**
* Number of steps
*/
public int steps;
public int steps = ActivitySample.NOT_MEASURED;
/**
* Number of seconds without activity (TBC)
*/
public int secondsInactive;
public int secondsInactive = ActivitySample.NOT_MEASURED;
/**
* Average Heart Rate in Beats Per Minute
*/
public int heartRate;
public int heartRate = ActivitySample.NOT_MEASURED;
private int age = 0;
/**
* Raw intensity calculated from calories
*/
public int intensity;
public int intensity = ActivitySample.NOT_MEASURED;
public HPlusDataRecordDaySlot(byte[] data, int age) {
super(data, TYPE_DAY_SLOT);
@ -85,6 +85,8 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
timestamp = (int) (slotTime.getTimeInMillis() / 1000L);
this.age = age;
intensity = (int) ((100*heartRate)/(208-0.7*age));
}
public String toString(){

View File

@ -24,6 +24,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -80,19 +81,24 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
int y = (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
battery = data[9];
calories = x + y; // KCal
heartRate = data[11] & 0xFF; // BPM
activeTime = (data[14] & 0xFF * 256) + (data[13] & 0xFF);
if(heartRate == 255) {
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_MEASURED;
if (battery == 255) {
battery = ActivitySample.NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
}
else {
intensity = (int) ((100*heartRate)/(208-0.7*age));
activityKind = ActivityKind.TYPE_UNKNOWN;
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_WORN;
} else {
heartRate = data[11] & 0xFF; // BPM
if (heartRate == 255) {
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
} else {
intensity = (int) ((100 * heartRate) / (208 - 0.7 * age));
activityKind = HPlusDataRecord.TYPE_REALTIME;
}
}
}

View File

@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusHealthSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay;
@ -123,7 +124,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
break;
}
if(gbDevice.getState() == GBDevice.State.NOT_CONNECTED){
if (gbDevice.getState() == GBDevice.State.NOT_CONNECTED) {
quit();
}
@ -137,11 +138,11 @@ class HPlusHandlerThread extends GBDeviceIoThread {
requestNextSleepData();
}
if(now.compareTo(mGetDaySummaryTime) > 0) {
if (now.compareTo(mGetDaySummaryTime) > 0) {
requestDaySummaryData();
}
if(now.compareTo(mHelloTime) > 0){
if (now.compareTo(mHelloTime) > 0) {
sendHello();
}
@ -178,7 +179,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
mDaySlotRecords.clear();
try {
if(!mHPlusSupport.isConnected())
if (!mHPlusSupport.isConnected())
mHPlusSupport.connect();
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
@ -188,7 +189,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
LOG.warn("HPlus: Synchronization exception: " + e);
}
@ -197,13 +198,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
}
public void sendHello(){
public void sendHello() {
try {
TransactionBuilder builder = new TransactionBuilder("hello");
builder.write(mHPlusSupport.ctrlCharacteristic, HPlusConstants.CMD_ACTION_HELLO);
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
mHelloTime = GregorianCalendar.getInstance();
@ -226,31 +227,30 @@ class HPlusHandlerThread extends GBDeviceIoThread {
try{
record = new HPlusDataRecordDaySlot(data, age);
} catch(IllegalArgumentException e){
} catch(IllegalArgumentException e) {
LOG.info((e.getMessage()));
return false;
}
Calendar now = GregorianCalendar.getInstance();
int nowSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + (now.get(Calendar.MINUTE) / 10);
if(record.slot == nowSlot){
if(mCurrentDaySlot != null && mCurrentDaySlot != record){
if (record.slot == nowSlot){
if (mCurrentDaySlot != null && mCurrentDaySlot != record) {
mCurrentDaySlot.accumulate(record);
mDaySlotRecords.add(mCurrentDaySlot);
mCurrentDaySlot = null;
}else{
} else {
//Store it to a temp variable as this is an intermediate value
mCurrentDaySlot = record;
if(!mSlotsInitialSync)
if (!mSlotsInitialSync)
return true;
}
}
if(mSlotsInitialSync) {
if (mSlotsInitialSync) {
//If the slot is in the future, actually it is from the previous day
//Subtract a day of seconds
if(record.slot > nowSlot){
if (record.slot > nowSlot) {
record.timestamp -= 3600 * 24;
}
@ -259,7 +259,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
//Ignore the current slot as it is incomplete
if(record.slot != nowSlot)
if (record.slot != nowSlot)
mDaySlotRecords.add(record);
//Still fetching ring buffer. Request the next slots
@ -271,14 +271,14 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
//Keep buffering
if(record.slot != 143)
if (record.slot != 143)
return true;
} else {
mGetDaySlotsTime = GregorianCalendar.getInstance();
mGetDaySlotsTime.add(Calendar.DAY_OF_MONTH, 1);
}
if(mDaySlotRecords.size() > 0) {
if (mDaySlotRecords.size() > 0) {
//Sort the samples
Collections.sort(mDaySlotRecords, new Comparator<HPlusDataRecordDaySlot>() {
public int compare(HPlusDataRecordDaySlot one, HPlusDataRecordDaySlot other) {
@ -286,6 +286,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
});
List<Integer> notWornSlots = new ArrayList<>();
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
List<HPlusHealthActivitySample> samples = new ArrayList<>();
@ -293,23 +295,62 @@ class HPlusHandlerThread extends GBDeviceIoThread {
for (HPlusDataRecordDaySlot storedRecord : mDaySlotRecords) {
//Invalid records (no data) will be ignored
if(!storedRecord.isValid())
if (!storedRecord.isValid())
continue;
HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp);
sample.setRawHPlusHealthData(storedRecord.getRawData());
sample.setSteps(storedRecord.steps);
sample.setRawIntensity(storedRecord.intensity);
sample.setHeartRate(storedRecord.heartRate);
sample.setRawKind(storedRecord.type);
sample.setRawIntensity(record.intensity);
sample.setProvider(provider);
samples.add(sample);
if (HPlusCoordinator.getAllDayHR(gbDevice.getAddress()) == HPlusConstants.ARG_HEARTRATE_ALLDAY_ON && storedRecord.heartRate == ActivitySample.NOT_MEASURED && storedRecord.steps <= 0) {
notWornSlots.add(sample.getTimestamp());
notWornSlots.add(sample.getTimestamp() + 10 * 60);
}
}
provider.getSampleDao().insertOrReplaceInTx(samples);
mDaySlotRecords.clear();
//Create an overlay with unused slots
if (notWornSlots.size() > 0) {
DaoSession session = dbHandler.getDaoSession();
Long userId = DBHelper.getUser(session).getId();
Long deviceId = DBHelper.getDevice(getDevice(), session).getId();
HPlusHealthActivityOverlayDao overlayDao = session.getHPlusHealthActivityOverlayDao();
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
int firstSlotTimestamp = notWornSlots.get(0);
int lastSlotTimestamp = notWornSlots.get(0);
int i = 1;
for (Integer timestamp : notWornSlots) {
//If it is the last of the samples or of the interruption period
if (timestamp - lastSlotTimestamp > 10 * 60) {
overlayList.add(new HPlusHealthActivityOverlay(firstSlotTimestamp, lastSlotTimestamp, ActivityKind.TYPE_NOT_WORN, deviceId, userId, null));
firstSlotTimestamp = timestamp;
}
lastSlotTimestamp = timestamp;
}
if (firstSlotTimestamp != lastSlotTimestamp)
overlayList.add(new HPlusHealthActivityOverlay(firstSlotTimestamp, lastSlotTimestamp, ActivityKind.TYPE_NOT_WORN, deviceId, userId, null));
overlayDao.insertOrReplaceInTx(overlayList);
}
} catch (GBException ex) {
LOG.info((ex.getMessage()));
} catch (Exception ex) {
@ -329,7 +370,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingSleepData(byte[] data){
public boolean processIncomingSleepData(byte[] data) {
HPlusDataRecordSleep record;
try{
@ -353,7 +394,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
List<HPlusDataRecord.RecordInterval> intervals = record.getIntervals();
for(HPlusDataRecord.RecordInterval interval : intervals){
for(HPlusDataRecord.RecordInterval interval : intervals) {
overlayList.add(new HPlusHealthActivityOverlay(interval.timestampFrom, interval.timestampTo, interval.activityKind, deviceId, userId, null));
}
@ -386,7 +427,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public boolean processRealtimeStats(byte[] data, int age) {
HPlusDataRecordRealtime record;
try{
try {
record = new HPlusDataRecordRealtime(data, age);
} catch(IllegalArgumentException e){
LOG.info((e.getMessage()));
@ -395,24 +436,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
//Skip duplicated messages as the device seems to send the same record multiple times
//This can be used to detect the user is moving (not sleeping)
if(prevRealTimeRecord != null && record.same(prevRealTimeRecord))
if (prevRealTimeRecord != null && record.same(prevRealTimeRecord))
return true;
prevRealTimeRecord = record;
getDevice().setBatteryLevel(record.battery);
//Skip when measuring heart rate
//Calories and Distance are updated and these values will be lost.
//Because a message with a valid Heart Rate will be provided, this loss very limited
if(record.heartRate == ActivityKind.TYPE_NOT_MEASURED) {
getDevice().setFirmwareVersion2("---");
getDevice().sendDeviceUpdateIntent(getContext());
}else {
getDevice().setFirmwareVersion2("" + record.heartRate);
getDevice().sendDeviceUpdateIntent(getContext());
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
@ -456,9 +486,9 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public boolean processDaySummary(byte[] data) {
HPlusDataRecordDaySummary record;
try{
try {
record = new HPlusDataRecordDaySummary(data);
} catch(IllegalArgumentException e){
} catch(IllegalArgumentException e) {
LOG.info((e.getMessage()));
return false;
}
@ -498,7 +528,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public boolean processVersion(byte[] data) {
int major, minor;
if(data.length >= 11){
if (data.length >= 11) {
major = data[10] & 0xFF;
minor = data[9] & 0xFF;
@ -506,8 +536,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
int hwMinor = data[1] & 0xFF;
getDevice().setFirmwareVersion2(hwMajor + "." + hwMinor);
mHPlusSupport.setUnicodeSupport((data[3] != 0));
}else {
mHPlusSupport.setUnicodeSupport(data[3] != 0);
} else {
major = data[2] & 0xFF;
minor = data[1] & 0xFF;
}
@ -526,7 +556,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
@ -548,23 +578,23 @@ class HPlusHandlerThread extends GBDeviceIoThread {
//Sync to current time
mGetDaySlotsTime = now;
if(mSlotsInitialSync) {
if(mLastSlotReceived == 143) {
if (mSlotsInitialSync) {
if (mLastSlotReceived == 143) {
mSlotsInitialSync = false;
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); //Sync complete. Delay timer forever
mLastSlotReceived = -1;
mLastSlotRequested = mLastSlotReceived + 1;
return;
}else {
} else {
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD);
}
}else{
} else {
//Sync complete. Delay timer forever
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
return;
}
if(mLastSlotReceived == 143)
if (mLastSlotReceived == 143)
mLastSlotReceived = -1;
byte hour = (byte) ((mLastSlotReceived + 1)/ 6);
@ -581,19 +611,19 @@ class HPlusHandlerThread extends GBDeviceIoThread {
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
}
/**
* Request a batch of data with the summary of the previous days
*/
public void requestDaySummaryData(){
public void requestDaySummaryData() {
try {
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
mGetDaySummaryTime = GregorianCalendar.getInstance();
@ -606,7 +636,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @param timestamp The sample timestamp
* @return The sample just created
*/
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp){
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
ivanovlev, João Paulo Barraca
ivanovlev, João Paulo Barraca, Pavel Motyrev
This file is part of Gadgetbridge.
@ -808,6 +808,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
private byte[] encodeStringToDevice(String s) {
List<Byte> outBytes = new ArrayList<Byte>();
Boolean unicode = HPlusCoordinator.getUnicodeSupport(this.gbDevice.getAddress());
LOG.info("Encode String: Unicode=" + unicode);
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);
@ -817,13 +819,14 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
cs = HPlusConstants.transliterateMap.get(c);
} else {
try {
if(HPlusCoordinator.getUnicodeSupport(this.gbDevice.getAddress()))
if(unicode)
cs = c.toString().getBytes("Unicode");
else
cs = c.toString().getBytes("GB2312");
} catch (UnsupportedEncodingException e) {
//Fallback. Result string may be strange, but better than nothing
cs = c.toString().getBytes();
LOG.error("Could not convert String to Bytes: " + e.getMessage());
}
}
for (int j = 0; j < cs.length; j++)
@ -884,7 +887,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
String DEVINFO_STEP = getContext().getString(R.string.chart_steps) + ": ";
String DEVINFO_DISTANCE = getContext().getString(R.string.distance) + ": ";
String DEVINFO_CALORY = getContext().getString(R.string.calories) + ": ";
String DEVINFO_HEART = getContext().getString(R.string.charts_legend_heartrate);
String DEVINFO_HEART = getContext().getString(R.string.charts_legend_heartrate) + ": ";
String info = "";
if (record.steps > 0) {

View File

@ -17,10 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.ParcelUuid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,158 +25,25 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class LiveviewIoThread extends GBDeviceIoThread {
public class LiveviewIoThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(LiveviewIoThread.class);
private static final UUID SERIAL = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private final LiveviewProtocol mLiveviewProtocol;
private final LiveviewSupport mLiveviewSupport;
private BluetoothAdapter mBtAdapter = null;
private BluetoothSocket mBtSocket = null;
private InputStream mInStream = null;
private OutputStream mOutStream = null;
private boolean mQuit = false;
@Override
public void quit() {
mQuit = true;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
}
}
private boolean mIsConnected = false;
public LiveviewIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol lvProtocol, LiveviewSupport lvSupport, BluetoothAdapter lvBtAdapter) {
super(gbDevice, context);
mLiveviewProtocol = (LiveviewProtocol) lvProtocol;
mBtAdapter = lvBtAdapter;
mLiveviewSupport = lvSupport;
super(gbDevice, context, lvProtocol, lvSupport, lvBtAdapter);
}
@Override
public synchronized void write(byte[] bytes) {
if (null == bytes)
return;
LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length));
try {
mOutStream.write(bytes);
mOutStream.flush();
} catch (IOException e) {
LOG.error("Error writing.", e);
}
}
@Override
public void run() {
mIsConnected = connect();
if (!mIsConnected) {
setUpdateState(GBDevice.State.NOT_CONNECTED);
return;
}
mQuit = false;
while (!mQuit) {
LOG.info("Ready for a new message exchange.");
try {
GBDeviceEvent deviceEvents[] = mLiveviewProtocol.decodeResponse(parseIncoming());
if (deviceEvents == null) {
LOG.info("unhandled message");
} else {
for (GBDeviceEvent deviceEvent : deviceEvents) {
if (deviceEvent == null) {
continue;
}
mLiveviewSupport.evaluateGBDeviceEvent(deviceEvent);
}
}
} catch (SocketTimeoutException ignore) {
LOG.debug("socket timeout, we can't help but ignore this");
} catch (IOException e) {
LOG.info(e.getMessage());
mIsConnected = false;
mBtSocket = null;
mInStream = null;
mOutStream = null;
LOG.info("Bluetooth socket closed, will quit IO Thread");
break;
}
}
mIsConnected = false;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
mBtSocket = null;
}
setUpdateState(GBDevice.State.NOT_CONNECTED);
}
@Override
protected boolean connect() {
GBDevice.State originalState = gbDevice.getState();
setUpdateState(GBDevice.State.CONNECTING);
try {
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress());
ParcelUuid uuids[] = btDevice.getUuids();
if (uuids == null) {
return false;
}
for (ParcelUuid uuid : uuids) {
LOG.info("found service UUID " + uuid);
}
mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid());
mBtSocket.connect();
mInStream = mBtSocket.getInputStream();
mOutStream = mBtSocket.getOutputStream();
setUpdateState(GBDevice.State.CONNECTED);
} catch (IOException e) {
LOG.error("Server socket cannot be started.");
//LOG.error(e.getMessage());
setUpdateState(originalState);
mInStream = null;
mOutStream = null;
mBtSocket = null;
return false;
}
write(mLiveviewProtocol.encodeSetTime());
setUpdateState(GBDevice.State.INITIALIZED);
return true;
}
private void setUpdateState(GBDevice.State state) {
gbDevice.setState(state);
gbDevice.sendDeviceUpdateIntent(getContext());
}
private byte[] parseIncoming() throws IOException {
protected byte[] parseIncoming(InputStream inputStream) throws IOException {
ByteArrayOutputStream msgStream = new ByteArrayOutputStream();
boolean finished = false;
@ -187,7 +51,7 @@ public class LiveviewIoThread extends GBDeviceIoThread {
byte[] incoming = new byte[1];
while (!finished) {
mInStream.read(incoming);
inputStream.read(incoming);
msgStream.write(incoming);
switch (state) {

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, atkyritsis, Carsten Pfeiffer,
Christian Fischer, Daniele Gobbetti, JohnnySun, Julien Pivotto, Kasha,
Sergey Trofimov, Steffen Liebergeld
Christian Fischer, Daniele Gobbetti, freezed-or-frozen, JohnnySun, Julien
Pivotto, Kasha, Sergey Trofimov, Steffen Liebergeld
This file is part of Gadgetbridge.
@ -1270,8 +1270,26 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
private void handleSensorData(byte[] value) {
int counter=0, step=0, axis1=0, axis2=0, axis3 =0;
/**
* Analyse and decode sensor data from ADXL362 accelerometer
* @param value to decode
* @return nothing
*
* Each axis raw value is 16bits long and look like : ttssvvvvvvvvvvvv
* tt : 2 bits for the type of data (00=x, 01=y, 10=z, 11=temperature)
* ss : sign of the value
* vvvvvvvvvvvv : accelerometer value encoded using two complements
*
* TODO: Because each accelerometer is different, all values should be calibrated with :
* a scale factor
* an offset factor
*/
private static void handleSensorData(byte[] value) {
int counter=0, step=0;
double xAxis=0.0, yAxis=0.0, zAxis=0.0;
double scale_factor = 1000.0;
double gravity = 9.81;
if ((value.length - 2) % 6 != 0) {
LOG.warn("GOT UNEXPECTED SENSOR DATA WITH LENGTH: " + value.length);
for (byte b : value) {
@ -1282,11 +1300,46 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
counter = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
for (int idx = 0; idx < ((value.length - 2) / 6); idx++) {
step = idx * 6;
axis1 = (value[step+2] & 0xff) | ((value[step+3] & 0xff) << 8);
axis2 = (value[step+4] & 0xff) | ((value[step+5] & 0xff) << 8);
axis3 = (value[step+6] & 0xff) | ((value[step+7] & 0xff) << 8);
// Analyse X-axis data
int xAxisRawValue = (value[step+2] & 0xff) | ((value[step+3] & 0xff) << 8);
int xAxisSign = (value[step+3] & 0x30) >> 4;
int xAxisType = (value[step+3] & 0xc0) >> 6;
if (xAxisSign == 0) {
xAxis = xAxisRawValue & 0xfff;
}
else {
xAxis = (xAxisRawValue & 0xfff) - 4097;
}
xAxis = (xAxis*1.0 / scale_factor) * gravity;
// Analyse Y-axis data
int yAxisRawValue = (value[step+4] & 0xff) | ((value[step+5] & 0xff) << 8);
int yAxisSign = (value[step+5] & 0x30) >> 4;
int yAxisType = (value[step+5] & 0xc0) >> 6;
if (yAxisSign == 0) {
yAxis = yAxisRawValue & 0xfff;
}
else {
yAxis = (yAxisRawValue & 0xfff) - 4097;
}
yAxis = (yAxis / scale_factor) * gravity;
// Analyse Z-axis data
int zAxisRawValue = (value[step+6] & 0xff) | ((value[step+7] & 0xff) << 8);
int zAxisSign = (value[step+7] & 0x30) >> 4;
int zAxisType = (value[step+7] & 0xc0) >> 6;
if (zAxisSign == 0) {
zAxis = zAxisRawValue & 0xfff;
}
else {
zAxis = (zAxisRawValue & 0xfff) - 4097;
}
zAxis = (zAxis / scale_factor) * gravity;
// Print results in log
LOG.info("READ SENSOR DATA VALUES: counter:"+counter+" step:"+step+" x-axis:"+ String.format("%.03f",xAxis)+" y-axis:"+String.format("%.03f",yAxis)+" z-axis:"+String.format("%.03f",zAxis)+";");
}
LOG.info("READ SENSOR DATA VALUES: counter:"+counter+" step:"+step+" axis1:"+axis1+" axis2:"+axis2+" axis3:"+axis3+";");
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -19,8 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
public enum FirmwareType {
FIRMWARE((byte) 0),
FONT((byte) 1),
UNKNOWN1((byte) 2),
UNKNOWN2((byte) 3),
RES((byte) 2),
GPS((byte) 3),
INVALID(Byte.MIN_VALUE);
private final byte value;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -53,7 +53,7 @@ public class Mi2FirmwareInfo {
0x4b
};
private static Map<Integer,String> crcToVersion = new HashMap<>();
protected static Map<Integer,String> crcToVersion = new HashMap<>();
static {
// firmware
crcToVersion.put(41899, "1.0.0.39");
@ -89,7 +89,7 @@ public class Mi2FirmwareInfo {
firmwareType = determineFirmwareType(bytes);
}
private FirmwareType determineFirmwareType(byte[] bytes) {
protected FirmwareType determineFirmwareType(byte[] bytes) {
if (ArrayUtils.startsWith(bytes, FT_HEADER)) {
return FirmwareType.FONT;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -18,7 +18,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
import android.bluetooth.BluetoothGattCharacteristic;
import android.support.annotation.NonNull;
import android.util.Log;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
@ -67,11 +66,11 @@ public class Mi2TextNotificationStrategy extends Mi2NotificationStrategy {
if (simpleNotification != null) {
switch (simpleNotification.getAlertCategory()) {
case Email:
return new byte[] { BLETypeConversions.fromUint8(MiBand2Service.ALERT_LEVEL_MESSAGE), BLETypeConversions.fromUint8(numAlerts)};
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.Email.getId()), BLETypeConversions.fromUint8(numAlerts)};
case InstantMessage:
return new byte[] { BLETypeConversions.fromUint8(MiBand2Service.ALERT_LEVEL_CUSTOM), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_CHAT};
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.CustomMiBand2.getId()), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_CHAT};
case News:
return new byte[] { BLETypeConversions.fromUint8(MiBand2Service.ALERT_LEVEL_CUSTOM), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_PENGUIN};
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.CustomMiBand2.getId()), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_PENGUIN};
}
}
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.SMS.getId()), BLETypeConversions.fromUint8(numAlerts)};

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, JohnnySun, Julien Pivotto, Kasha, Sergey Trofimov,
Steffen Liebergeld
Fischer, Daniele Gobbetti, JohnnySun, José Rebelo, Julien Pivotto, Kasha,
Sergey Trofimov, Steffen Liebergeld
This file is part of Gadgetbridge.
@ -36,8 +36,10 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -50,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInf
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.DoNotDisturb;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
@ -298,7 +301,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return this;
}
private NotificationStrategy getNotificationStrategy() {
public NotificationStrategy getNotificationStrategy() {
String firmwareVersion = getDevice().getFirmwareVersion();
if (firmwareVersion != null) {
Version ver = new Version(firmwareVersion);
@ -432,7 +435,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
}
}
private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
protected void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
try {
TransactionBuilder builder = performInitialized(task);
Prefs prefs = GBApplication.getPrefs();
@ -526,7 +529,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
performPreferredNotification(origin + " received", origin, simpleNotification, alertLevel, null);
}
private void onAlarmClock(NotificationSpec notificationSpec) {
protected void onAlarmClock(NotificationSpec notificationSpec) {
alarmClockRinging = true;
AbortTransactionAction abortAction = new StopNotificationAction(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) {
@Override
@ -805,7 +808,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return false;
}
private void handleButtonPressed(byte[] value) {
public void handleButtonPressed(byte[] value) {
LOG.info("Button pressed");
logMessageContent(value);
}
@ -1075,12 +1078,35 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
case MiBandConst.PREF_MI2_DATEFORMAT:
setDateDisplay(builder);
break;
case MiBandConst.PREF_MI2_GOAL_NOTIFICATION:
setGoalNotification(builder);
break;
case MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT:
setActivateDisplayOnLiftWrist(builder);
break;
case MiBandConst.PREF_MI2_DISPLAY_ITEMS:
setDisplayItems(builder);
break;
case MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO:
setRotateWristToSwitchInfo(builder);
break;
case ActivityUser.PREF_USER_STEPS_GOAL:
setFitnessGoal(builder);
break;
case MiBandConst.PREF_MI2_DO_NOT_DISTURB:
case MiBandConst.PREF_MI2_DO_NOT_DISTURB_START:
case MiBandConst.PREF_MI2_DO_NOT_DISTURB_END:
setDoNotDisturb(builder);
break;
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_START:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_END:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_START:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_END:
setInactivityWarnings(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
@ -1128,6 +1154,17 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return this;
}
private MiBand2Support setGoalNotification(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getGoalNotification();
LOG.info("Setting goal notification to " + enable);
if (enable) {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_GOAL_NOTIFICATION);
} else {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_GOAL_NOTIFICATION);
}
return this;
}
private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist();
LOG.info("Setting activate display on lift wrist to " + enable);
@ -1139,6 +1176,137 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return this;
}
private MiBand2Support setDisplayItems(TransactionBuilder builder) {
Set<String> pages = MiBand2Coordinator.getDisplayItems();
LOG.info("Setting display items to " + (pages == null ? "none" : pages));
byte[] data = MiBand2Service.COMMAND_CHANGE_SCREENS.clone();
if (pages != null) {
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_STEPS)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_STEPS;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_DISTANCE)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_DISTANCE;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_CALORIES)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_CALORIES;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_HEART_RATE)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_HEART_RATE;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_BATTERY)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_BATTERY;
}
}
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
return this;
}
private MiBand2Support setRotateWristToSwitchInfo(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getRotateWristToSwitchInfo();
LOG.info("Setting rotate wrist to cycle info to " + enable);
if (enable) {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_ROTATE_WRIST_TO_SWITCH_INFO);
} else {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_ROTATE_WRIST_TO_SWITCH_INFO);
}
return this;
}
private MiBand2Support setDisplayCaller(TransactionBuilder builder) {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_DISPLAY_CALLER);
return this;
}
private MiBand2Support setDoNotDisturb(TransactionBuilder builder) {
DoNotDisturb doNotDisturb = MiBand2Coordinator.getDoNotDisturb(getContext());
LOG.info("Setting do not disturb to " + doNotDisturb);
switch (doNotDisturb) {
case OFF:
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DO_NOT_DISTURB_OFF);
break;
case AUTOMATIC:
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DO_NOT_DISTURB_AUTOMATIC);
break;
case SCHEDULED:
byte[] data = MiBand2Service.COMMAND_DO_NOT_DISTURB_SCHEDULED.clone();
Calendar calendar = GregorianCalendar.getInstance();
Date start = MiBand2Coordinator.getDoNotDisturbStart();
calendar.setTime(start);
data[MiBand2Service.DND_BYTE_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.DND_BYTE_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
Date end = MiBand2Coordinator.getDoNotDisturbEnd();
calendar.setTime(end);
data[MiBand2Service.DND_BYTE_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.DND_BYTE_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
break;
}
return this;
}
private MiBand2Support setInactivityWarnings(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getInactivityWarnings();
LOG.info("Setting inactivity warnings to " + enable);
if (enable) {
byte[] data = MiBand2Service.COMMAND_ENABLE_INACTIVITY_WARNINGS.clone();
int threshold = MiBand2Coordinator.getInactivityWarningsThreshold();
data[MiBand2Service.INACTIVITY_WARNINGS_THRESHOLD] = (byte) threshold;
Calendar calendar = GregorianCalendar.getInstance();
boolean enableDnd = MiBand2Coordinator.getInactivityWarningsDnd();
Date intervalStart = MiBand2Coordinator.getInactivityWarningsStart();
Date intervalEnd = MiBand2Coordinator.getInactivityWarningsEnd();
Date dndStart = MiBand2Coordinator.getInactivityWarningsDndStart();
Date dndEnd = MiBand2Coordinator.getInactivityWarningsDndEnd();
// The first interval always starts when the warnings interval starts
calendar.setTime(intervalStart);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
if(enableDnd) {
// The first interval ends when the dnd interval starts
calendar.setTime(dndStart);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
// The second interval starts when the dnd interval ends
calendar.setTime(dndEnd);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
// ... and it ends when the warnings interval ends
calendar.setTime(intervalEnd);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
} else {
// No Dnd, use the first interval
calendar.setTime(intervalEnd);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY);
data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE);
}
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
} else {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_INACTIVITY_WARNINGS);
}
return this;
}
public void phase2Initialize(TransactionBuilder builder) {
LOG.info("phase2Initialize...");
enableFurtherNotifications(builder, true);
@ -1147,7 +1315,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
setTimeFormat(builder);
setWearLocation(builder);
setFitnessGoal(builder);
setDisplayItems(builder);
setDoNotDisturb(builder);
setRotateWristToSwitchInfo(builder);
setActivateDisplayOnLiftWrist(builder);
setDisplayCaller(builder);
setGoalNotification(builder);
setInactivityWarnings(builder);
setHeartrateSleepSupport(builder);
}
}

View File

@ -20,6 +20,8 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.v4.util.TimeUtils;
import android.text.format.DateUtils;
import android.widget.Toast;
import org.slf4j.Logger;
@ -66,8 +68,9 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
private List<MiBandActivitySample> samples = new ArrayList<>(60*24); // 1day per default
private byte lastPacketCounter = -1;
private byte lastPacketCounter;
private Calendar startTimestamp;
private int fetchCount;
public FetchActivityOperation(MiBand2Support support) {
super(support);
@ -83,12 +86,25 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
@Override
protected void doPerform() throws IOException {
startFetching();
}
private void startFetching() throws IOException {
samples.clear();
lastPacketCounter = -1;
TransactionBuilder builder = performInitialized("fetching activity data");
getSupport().setLowLatency(builder);
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
if (fetchCount == 0) {
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
}
fetchCount++;
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
builder.notify(characteristicActivityData, false);
BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4);
builder.notify(characteristicFetch, true);
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)));
@ -136,13 +152,40 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
}
private void handleActivityFetchFinish() {
LOG.info("Fetching activity data has finished.");
saveSamples();
LOG.info("Fetching activity data has finished round " + fetchCount);
GregorianCalendar lastSyncTimestamp = saveSamples();
if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
try {
startFetching();
return;
} catch (IOException ex) {
LOG.error("Error starting another round of fetching activity data", ex);
}
}
operationFinished();
unsetBusy();
}
private void saveSamples() {
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
if (fetchCount > 5) {
LOG.warn("Already jave 5 fetch rounds, not doing another one.");
return false;
}
if (DateUtils.isToday(lastSyncTimestamp.getTimeInMillis())) {
LOG.info("Hopefully no further fetch needed, last synced timestamp is from today.");
return false;
}
if (lastSyncTimestamp.getTimeInMillis() > System.currentTimeMillis()) {
LOG.warn("Not doing another fetch since last synced timestamp is in the future: " + DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return false;
}
LOG.info("Doing another fetch since last sync timestamp is still too old: " + DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return true;
}
private GregorianCalendar saveSamples() {
if (samples.size() > 0) {
// save all the samples that we got
try (DBHandler handler = GBApplication.acquireDB()) {
@ -168,6 +211,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
saveLastSyncTimestamp(timestamp);
LOG.info("Mi2 activity data: last sample timestamp: " + DateTimeUtils.formatDateTime(timestamp.getTime()));
return timestamp;
} catch (Exception ex) {
GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR);
@ -175,6 +219,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
samples.clear();
}
}
return null;
}
/**

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -48,11 +48,11 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class);
private final Uri uri;
private final BluetoothGattCharacteristic fwCControlChar;
private final BluetoothGattCharacteristic fwCDataChar;
final Prefs prefs = GBApplication.getPrefs();
private Mi2FirmwareInfo firmwareInfo;
protected final Uri uri;
protected final BluetoothGattCharacteristic fwCControlChar;
protected final BluetoothGattCharacteristic fwCDataChar;
protected final Prefs prefs = GBApplication.getPrefs();
protected Mi2FirmwareInfo firmwareInfo;
public UpdateFirmwareOperation(Uri uri, MiBand2Support support) {
super(support);
@ -82,7 +82,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
//the firmware will be sent by the notification listener if the band confirms that the metadata are ok.
}
private void done() {
protected void done() {
LOG.info("Operation done.");
operationFinished();
unsetBusy();
@ -163,7 +163,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
done();
}
}
private void displayMessage(Context context, String message, int duration, int severity) {
protected void displayMessage(Context context, String message, int duration, int severity) {
getSupport().handleGBDeviceEvent(new GBDeviceEventDisplayMessage(message, duration, severity));
}

View File

@ -28,7 +28,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -158,9 +157,11 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
int version = (int) pair.second;
LOG.info("got version: " + ((float) version / 10.0f));
ctrl_message |= CTRL_VERSION_DONE;
} else if (pair.first.equals(keyBase)) {// fix timestamp
TimeZone tz = SimpleTimeZone.getDefault();
recording_base_timestamp = (int) pair.second - (tz.getOffset(System.currentTimeMillis())) / 1000;
} else if (pair.first.equals(keyBase)) {
recording_base_timestamp = (int) pair.second;
if (mPebbleProtocol.mFwMajor < 3) {
recording_base_timestamp -= SimpleTimeZone.getDefault().getOffset(recording_base_timestamp * 1000L) / 1000;
}
LOG.info("got base: " + recording_base_timestamp);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
} else if (pair.first.equals(keyAutoReset)) {

View File

@ -156,7 +156,7 @@ class PebbleIoThread extends GBDeviceIoThread {
}
}
} catch (IOException e) {
e.printStackTrace();
LOG.warn("error while connecting: " + e.getMessage(), e);
gbDevice.setState(originalState);
gbDevice.sendDeviceUpdateIntent(getContext());

View File

@ -87,6 +87,8 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
byteArray[i] = ((Integer) jsonArray.get(i)).byteValue();
}
object = byteArray;
} else if (object instanceof Boolean) {
object = (short) (((Boolean) object) ? 1 : 0);
}
pairs.add(new Pair<>(Integer.parseInt(keyStr), object));
}

View File

@ -164,7 +164,14 @@ class PebbleGATTClient extends BluetoothGattCallback {
if (doPairing) {
BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(PAIRING_TRIGGER_CHARACTERISTIC);
if ((characteristic.getProperties() & PROPERTY_WRITE) != 0) {
characteristic.setValue(new byte[]{1});
LOG.info("This seems to be a >=4.0 FW Pebble, writing to pairing trigger");
// flags:
// 0 - always 1
// 1 - unknown
// 2 - always 0
// 3 - unknown, set on kitkat (seems to help to get a "better" pairing)
// 4 - unknown, set on some phones
characteristic.setValue(new byte[]{9});
gatt.writeCharacteristic(characteristic);
} else {
LOG.info("This seems to be some <4.0 FW Pebble, reading pairing trigger");

View File

@ -39,6 +39,7 @@ public class GBCallControlReceiver extends BroadcastReceiver {
GBDeviceEventCallControl.Event callCmd = GBDeviceEventCallControl.Event.values()[intent.getIntExtra("event", 0)];
switch (callCmd) {
case END:
case REJECT:
case START:
try {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
@ -46,7 +47,7 @@ public class GBCallControlReceiver extends BroadcastReceiver {
Method method = clazz.getDeclaredMethod("getITelephony");
method.setAccessible(true);
ITelephony telephonyService = (ITelephony) method.invoke(telephonyManager);
if (callCmd == GBDeviceEventCallControl.Event.END) {
if (callCmd == GBDeviceEventCallControl.Event.END || callCmd == GBDeviceEventCallControl.Event.REJECT) {
telephonyService.endCall();
} else {
telephonyService.answerRingingCall();

View File

@ -16,12 +16,20 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
public class AndroidUtils {
public static ParcelUuid[] toParcelUUids(Parcelable[] uuids) {
if (uuids == null) {
@ -61,4 +69,48 @@ public class AndroidUtils {
return false;
}
}
public static void setLanguage(Activity activity, Locale language) {
Configuration config = new Configuration();
config.setLocale(language);
// FIXME: I have no idea what I am doing
activity.getBaseContext().getResources().updateConfiguration(config, activity.getBaseContext().getResources().getDisplayMetrics());
activity.recreate();
}
/**
* Returns the theme dependent text color as a css-style hex string.
* @param context the context to access the colour
*/
public static String getTextColorHex(Context context) {
int color;
if (GBApplication.isDarkThemeEnabled()) {
color = context.getResources().getColor(R.color.primarytext_dark);
} else {
color = context.getResources().getColor(R.color.primarytext_light);
}
return colorToHex(color);
}
/**
* Returns the theme dependent background color as a css-style hex string.
* @param context the context to access the colour
*/
public static String getBackgroundColorHex(Context context) {
int color;
if (GBApplication.isDarkThemeEnabled()) {
color = context.getResources().getColor(R.color.cardview_dark_background);
} else {
color = context.getResources().getColor(R.color.cardview_light_background);
}
return colorToHex(color);
}
private static String colorToHex(int color) {
return "#"
+ Integer.toHexString(Color.red(color))
+ Integer.toHexString(Color.green(color))
+ Integer.toHexString(Color.blue(color));
}
}

View File

@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipCooordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
@ -184,6 +185,7 @@ public class DeviceHelper {
private List<DeviceCoordinator> createCoordinators() {
List<DeviceCoordinator> result = new ArrayList<>();
result.add(new AmazfitBipCooordinator()); // Note: AmazfitBip must come before MiBand2 because detection is hacky, atm
result.add(new MiBand2Coordinator()); // Note: MiBand2 must come before MiBand because detection is hacky, atm
result.add(new MiBandCoordinator());
result.add(new PebbleCoordinator());

View File

@ -76,7 +76,7 @@ public class GB {
.setContentIntent(pendingIntent)
.setOngoing(true);
if (GBApplication.isRunningLollipopOrLater()) {
builder.setVisibility(Notification.VISIBILITY_PUBLIC);
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
}
if (GBApplication.minimizeNotification()) {
builder.setPriority(Notification.PRIORITY_MIN);
@ -268,7 +268,7 @@ public class GB {
notificationIntent, 0);
NotificationCompat.Builder nb = new NotificationCompat.Builder(context)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(text)
.setContentIntent(pendingIntent)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -21,6 +21,7 @@ import java.util.Date;
public class GBPrefs {
public static final String PACKAGE_BLACKLIST = "package_blacklist";
public static final String CALENDAR_BLACKLIST = "calendar_blacklist";
public static final String AUTO_RECONNECT = "general_autocreconnect";
private static final String AUTO_START = "general_autostartonboot";
private static final boolean AUTO_START_DEFAULT = true;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Alberto, Carsten Pfeiffer
/* Copyright (C) 2017 Alberto, Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -17,6 +17,12 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.SharedPreferences;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
@ -27,12 +33,6 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import android.content.SharedPreferences;
import android.util.Xml;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class ImportExportSharedPreferences {
@ -125,12 +125,19 @@ public class ImportExportSharedPreferences {
editor.putString(key, text);
} else if (HASHSET.equals(name)) {
if (key.equals(GBPrefs.PACKAGE_BLACKLIST)) {
Set<String> blacklist = new HashSet<>();
Set<String> apps_blacklist = new HashSet<>();
text=text.replace("[","").replace("]","");
for (int z=0;z<text.split(",").length;z++){
blacklist.add(text.split(",")[z].trim());
apps_blacklist.add(text.split(",")[z].trim());
}
GBApplication.setBlackList(blacklist);
GBApplication.setAppsBlackList(apps_blacklist);
} else if (key.equals(GBPrefs.CALENDAR_BLACKLIST)) { //TODO: untested
Set<String> calendars_blacklist = new HashSet<>();
text = text.replace("[", "").replace("]", "");
for (int z = 0; z < text.split(",").length; z++) {
calendars_blacklist.add(text.split(",")[z].trim());
}
GBApplication.setCalendarsBlackList(calendars_blacklist);
}
} else if (!PREFERENCES.equals(name)) {
throw new Exception("Unkown type " + name);

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2017 ivanovlev, Yaron Shahrabani
/* Copyright (C) 2017 Andreas Shimokawa, ivanovlev, lazarosfs, Yaron
Shahrabani
This file is part of Gadgetbridge.
@ -28,7 +29,12 @@ public class LanguageUtils {
private static Map<Character, String> transliterateMap = new HashMap<Character, String>(){
{
//extended ASCII characters
put('æ', "ae"); put('œ', "oe"); put('ß', "B"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\"");
put('æ', "ae"); put('œ', "oe"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\"");
//german characters
put('ä',"ae"); put('ö',"oe"); put('ü',"ue");
put('Ä',"Ae"); put('Ö',"Oe"); put('Ü',"Üe");
put('ß',"ss"); put('ẞ',"SS");
//russian chars
put('а', "a"); put('б', "b"); put('в', "v"); put('г', "g"); put('д', "d"); put('е', "e"); put('ё', "jo"); put('ж', "zh");
@ -42,7 +48,19 @@ public class LanguageUtils {
put('ט', "t"); put('י', "y"); put('כ', "c"); put('ל', "l"); put('מ', "m"); put('נ', "n"); put('ס', "s"); put('ע', "'");
put('פ', "p"); put('צ', "ts"); put('ק', "k"); put('ר', "r"); put('ש', "sh"); put('ת', "th"); put('ף', "f"); put('ץ', "ts");
put('ך', "ch");put('ם', "m");put('ן', "n");
//continue for other languages...
// greek chars
put('α',"a");put('ά',"a");put('β',"v");put('γ',"g");put('δ',"d");put('ε',"e");put('έ',"e");put('ζ',"z");put('η',"i");
put('ή',"i");put('θ',"th");put('ι',"i");put('ί',"i");put('ϊ',"i");put('ΐ',"i");put('κ',"k");put('λ',"l");put('μ',"m");
put('ν',"n");put('ξ',"ks");put('ο',"o");put('ό',"o");put('π',"p");put('ρ',"r");put('σ',"s");put('ς',"s");put('τ',"t");
put('υ',"y");put('ύ',"y");put('ϋ',"y");put('ΰ',"y");put('φ',"f");put('χ',"ch");put('ψ',"ps");put('ω',"o");put('ώ',"o");
put('Α',"A");put('Ά',"A");put('Β',"B");put('Γ',"G");put('Δ',"D");put('Ε',"E");put('Έ',"E");put('Ζ',"Z");put('Η',"I");
put('Ή',"I");put('Θ',"TH");put('Ι',"I");put('Ί',"I");put('Ϊ',"I");put('Κ',"K");put('Λ',"L");put('Μ',"M");put('Ν',"N");
put('Ξ',"KS");put('Ο',"O");put('Ό',"O");put('Π',"P");put('Ρ',"R");put('Σ',"S");put('Τ',"T");put('Υ',"Y");put('Ύ',"Y");
put('Ϋ',"Y");put('Φ',"F");put('Χ',"CH");put('Ψ',"PS");put('Ω',"O");put('Ώ',"O");
//TODO: these must be configurabe. If someone wants to transliterate cyrillic it does not mean his device has no German umlauts
//all or nothing is really bad here
}
};

View File

@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.SharedPreferences;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;
/**
@ -162,4 +163,16 @@ public class Prefs {
public SharedPreferences getPreferences() {
return preferences;
}
/**
* Ugly workaround for Set<String> preferences not consistently applying.
* @param editor
* @param preference
* @param value
*/
public static void putStringSet(SharedPreferences.Editor editor, String preference, HashSet<String> value) {
editor.putStringSet(preference, null);
editor.commit();
editor.putStringSet(preference, new HashSet<>(value));
}
}

View File

@ -0,0 +1,116 @@
/* Copyright (C) 2017 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.DialogPreference;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TimePicker;
public class TimePreference extends DialogPreference {
private int hour = 0;
private int minute = 0;
private TimePicker picker = null;
public TimePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected View onCreateDialogView() {
picker = new TimePicker(getContext());
picker.setIs24HourView(DateFormat.is24HourFormat(getContext()));
picker.setPadding(0, 50, 0, 50);
return picker;
}
@Override
protected void onBindDialogView(View v) {
super.onBindDialogView(v);
picker.setCurrentHour(hour);
picker.setCurrentMinute(minute);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
hour = picker.getCurrentHour();
minute = picker.getCurrentMinute();
String time = getTime24h();
if (callChangeListener(time)) {
persistString(time);
updateSummary();
}
}
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getString(index);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
String time;
if (restoreValue) {
if (defaultValue == null) {
time = getPersistedString("00:00");
} else {
time = getPersistedString(defaultValue.toString());
}
} else {
time = defaultValue.toString();
}
String[] pieces = time.split(":");
hour = Integer.parseInt(pieces[0]);
minute = Integer.parseInt(pieces[1]);
updateSummary();
}
public void updateSummary() {
if (DateFormat.is24HourFormat(getContext()))
setSummary(getTime24h());
else
setSummary(getTime12h());
}
public String getTime24h() {
return String.format("%02d", hour) + ":" + String.format("%02d", minute);
}
public String getTime12h() {
String suffix = hour < 12 ? " AM" : " PM";
int h = hour > 12 ? hour - 12 : hour;
return String.valueOf(h) + ":" + String.format("%02d", minute) + suffix;
}
}

View File

@ -28,7 +28,9 @@ public class Version implements Comparable<Version> {
public Version(String version) {
if(version == null)
throw new IllegalArgumentException("Version can not be null");
if(!version.matches("[0-9]+(\\.[0-9]+)*"))
version = version.trim();
if (!version.matches("[0-9]+(\\.[0-9]+)*"))
throw new IllegalArgumentException("Invalid version format");
this.version = version;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Some files were not shown because too many files have changed in this diff Show More