Merge branch 'master' into health-new-database

This commit is contained in:
Andreas Shimokawa 2017-05-09 14:10:55 +02:00
commit 3bb255eb47
140 changed files with 3375 additions and 1487 deletions

View File

@ -1,23 +1,53 @@
###Changelog
### Changelog
###Version 0.18.4
#### 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
* 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
* Improve reconnection with BLE devices
* Improve generic notification reliability by trying to restart the notification listener when stale/crashed
* Other small bugfixes
#### 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
* Visual improvements of the pie charts
* Add filter by name in the App blacklist activity
* Pebble: improve compatibility with watch app configuration pages
* Pebble: display battery percentage (will only update once an hour)
* 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
* 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
@ -33,13 +63,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)
@ -52,23 +82,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
@ -86,7 +116,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
@ -97,15 +127,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
@ -113,20 +143,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)
@ -134,7 +164,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
@ -143,16 +173,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
@ -161,16 +191,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
@ -181,36 +211,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
@ -219,10 +249,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
@ -235,19 +265,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)
@ -255,7 +285,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
@ -263,7 +293,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
@ -272,7 +302,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
@ -280,14 +310,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
@ -297,7 +327,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)
@ -307,24 +337,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
@ -333,19 +363,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)
@ -355,17 +385,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
@ -373,11 +403,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)
@ -389,7 +419,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
@ -402,7 +432,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)
@ -410,26 +440,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)
@ -437,23 +467,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)
@ -462,13 +492,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
@ -476,7 +506,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
@ -484,11 +514,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)
@ -497,7 +527,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)
@ -507,7 +537,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)
@ -515,7 +545,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
@ -525,24 +555,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)
@ -556,33 +586,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
@ -590,7 +620,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
@ -598,31 +628,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

@ -2,7 +2,7 @@
names ()
{
echo -e "\n exit;\n**Contributors (sorted by number of commits):**\n";
git log --format='%aN:%aE' origin/master | sed 's/@users.github.com/@users.noreply.github.com/g' | awk 'BEGIN{FS=":"}{ct[$2]+=1;if (length($1) > length(e[$2])) {e[$2]=$1}}END{for (i in e) { n[e[i]]=i;c[e[i]]+=ct[i] }; for (a in n) print c[a]"\t* "a" <"n[a]">";}' | sort -n -r | cut -f 2-
git log --format='%aN:%ae' origin/master | grep -Ev "FYG_.*_bot_ignore_me" | sed 's/@users.github.com/@users.noreply.github.com/g' | awk 'BEGIN{FS=":"}{ct[$1]+=1;if (length($2) > length(e[$1])) {e[$1]=$2}}END{for (i in e) { n[i]=e[i];c[i]+=ct[i] }; for (a in e) print c[a]"\t* "a" <"n[a]">";}' | sort -n -r | cut -f 2-
}
quine ()
{
@ -33,14 +33,17 @@
* Sergey Trofimov <sarg@sarg.org.ru>
* JohnnySun <bmy001@gmail.com>
* Uwe Hermann <uwe@hermann-uwe.de>
* Alberto <albertsal83@gmail.com>
* 0nse <0nse@users.noreply.github.com>
* Gergely Peidl <gergely@peidl.net>
* Christian Fischer <sw-dev@computerlyrik.de>
* 6arms1leg <m.brnsfld@googlemail.com>
* walkjivefly <mark@walkjivefly.com>
* Normano64 <per.bergqwist@gmail.com>
* Avamander <Avamander@users.noreply.github.com>
* Ⲇⲁⲛⲓ Φi <daniphii@outlook.com>
* Yar <yaroslav.isakov@gmail.com>
* Yaron Shahrabani <sh.yaron@gmail.com>
* xzovy <caleb@caleb-cooper.net>
* xphnx <xphnx@users.noreply.github.com>
* Tarik Sekmen <tarik@ilixi.org>
@ -57,6 +60,7 @@
* Hasan Ammar <ammarh@gmail.com>
* Gilles MOREL <contact@gilles-morel.fr>
* Gilles Émilien MOREL <Almtesh@users.noreply.github.com>
* Daniel Hauck <maill@dhauck.eu>
* Chris Perelstein <chris.perelstein@gmail.com>
* Carlos Ferreira <calbertoferreira@gmail.com>
* atkyritsis <at.kyritsis@gmail.com>

View File

@ -17,6 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.daogen;
import de.greenrobot.daogenerator.DaoGenerator;
import de.greenrobot.daogenerator.Entity;
import de.greenrobot.daogenerator.Index;
import de.greenrobot.daogenerator.Property;
import de.greenrobot.daogenerator.Schema;
@ -40,6 +41,7 @@ public class GBDaoGenerator {
private static final String TIMESTAMP_FROM = "timestampFrom";
private static final String TIMESTAMP_TO = "timestampTo";
public static void main(String[] args) throws Exception {
Schema schema = new Schema(16, MAIN_PACKAGE + ".entities");
@ -65,6 +67,8 @@ public class GBDaoGenerator {
addHPlusHealthActivityKindOverlay(schema, user, device);
addHPlusHealthActivitySample(schema, user, device);
addCalendarSyncState(schema, device);
new DaoGenerator().generateAll(schema, "app/src/main/java");
}
@ -292,6 +296,20 @@ public class GBDaoGenerator {
activitySample.addToOne(user, userId);
}
private static void addCalendarSyncState(Schema schema, Entity device) {
Entity calendarSyncState = addEntity(schema, "CalendarSyncState");
calendarSyncState.addIdProperty();
Property deviceId = calendarSyncState.addLongProperty("deviceId").notNull().getProperty();
Property calendarEntryId = calendarSyncState.addLongProperty("calendarEntryId").notNull().getProperty();
Index indexUnique = new Index();
indexUnique.addProperty(deviceId);
indexUnique.addProperty(calendarEntryId);
indexUnique.makeUnique();
calendarSyncState.addIndex(indexUnique);
calendarSyncState.addToOne(device, deviceId);
calendarSyncState.addIntProperty("hash").notNull();
}
private static Property findProperty(Entity entity, String propertyName) {
for (Property prop : entity.getProperties()) {
if (propertyName.equals(prop.getPropertyName())) {

View File

@ -15,7 +15,7 @@ need to create an account and transmit any of your data to the vendor's servers.
## 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, Pebble Time 2 (experimental, PAIR WITHIN GADGETBRIDGE) [Wiki section about pebble](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble), most parts apply to Pebble 2 as well
* 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)
@ -26,13 +26,12 @@ need to create an account and transmit any of your data to the vendor's servers.
* Incoming calls notification and display
* Outgoing call display
* Reject/hangup calls
* Reject calls (optionally with predefined texts) / hangup calls
* SMS notification
* K-9 Mail notification support
* Support for generic notifications (above filtered out)
* Support for up to 16 predefined replies for SMS and Android Wear compatible notifications (experimental, tested with Signal)
* Support for generic notifications
* Support for up to 16 predefined replies for SMS and Android Wear compatible notifications (experimental, tested with Signal and Conversations)
* Dismiss individual notifications, mute or open corresponding app on phone from the action menu (generic notifications)
* Dismiss all notifications from the action menu (non-generic notifications)
* Dismiss all notifications from the action menu (SMS and PebbleKit notifications)
* Music playback info (artist, album, track)
* Music control: play/pause, next track, previous track, volume up, volume down
* List and remove installed apps/watchfaces
@ -41,7 +40,8 @@ need to create an account and transmit any of your data to the vendor's servers.
* Install language files (.pbl)
* Take and share screenshots from the Pebble's screen
* PebbleKit support for 3rd Party Android Apps (experimental)
* Fetch activity data from Pebble Health, Misfit and Morpheuz (experimental)
* Fetch activity data from Pebble Health
* Build-in support for Misfit and Morpheuz (experimental)
* Configure watchfaces / apps (limited compatibility, experimental)
## Notes about Firmware >=3.0 (Pebble Time, updated OG)

View File

@ -26,8 +26,8 @@ android {
targetSdkVersion 25
// note: always bump BOTH versionCode and versionName!
versionName "0.18.4"
versionCode 91
versionName "0.19.2"
versionCode 95
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -53,7 +53,7 @@ android {
}
pmd {
toolVersion = '5.5.1'
toolVersion = '5.5.5'
}
dependencies {
@ -61,7 +61,7 @@ dependencies {
// testCompile 'ch.qos.logback:logback-core:1.1.3'
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.9.5"
testCompile "org.robolectric:robolectric:3.2.2"
testCompile "org.robolectric:robolectric:3.3.2"
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.3.1'
@ -69,14 +69,16 @@ dependencies {
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.github.tony19:logback-android-classic:1.1.1-4'
compile 'com.github.tony19:logback-android-classic:1.1.1-6'
compile 'org.slf4j:slf4j-api:1.7.7'
compile 'com.github.PhilJay:MPAndroidChart:v3.0.1'
compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'
compile 'com.github.pfichtner:durationformatter:0.1.1'
compile 'de.cketti.library.changelog:ckchangelog:1.2.2'
compile 'net.e175.klaus:solarpositioning:0.0.9'
compile 'com.github.freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
compile 'org.apache.commons:commons-lang3:3.4'
// use pristine greendao instead of our custom version, since our custom jitpack-packaged
// version contains way too much and our custom patches are in the generator only.
compile 'org.greenrobot:greendao:2.2.1'
compile 'org.apache.commons:commons-lang3:3.5'
// compile project(":DaoCore")
}

View File

@ -251,6 +251,7 @@
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service android:name=".service.NotificationCollectorMonitorService" />
<service android:name=".service.DeviceCommunicationService" />
<receiver

View File

@ -1,7 +1,14 @@
navigator.geolocation.getCurrentPosition = function(success, failure) { //override because default implementation requires GPS permission
success(JSON.parse(GBjs.getCurrentPosition()));
failure({ code: 2, message: "POSITION_UNAVAILABLE"});
var reportedPositionFailures = 0;
navigator.geolocation.getCurrentPosition = function(success, failure, options) { //override because default implementation requires GPS permission
geoposition = JSON.parse(GBjs.getCurrentPosition());
if(options && options.maximumAge && (geoposition.timestamp < Date.now() - options.maximumAge) && reportedPositionFailures <= 10 ) {
reportedPositionFailures++;
failure({ code: 2, message: "POSITION_UNAVAILABLE"});
} else {
reportedPositionFailures = 0;
success(geoposition);
}
}
if (window.Storage){
@ -201,7 +208,7 @@ var storedPreset = GBjs.getAppStoredPreset();
document.addEventListener('DOMContentLoaded', function(){
if (jsConfigFile != null) {
loadScript(jsConfigFile, function() {
Pebble.evaluate('ready');
Pebble.evaluate('ready', [{'type': "ready"}]); //callback object apparently needed by some watchfaces
if (getURLVariable('config') == 'true') {
showStep("step2");
var json_string = getURLVariable('json');

View File

@ -40,6 +40,7 @@ import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@ -53,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.NotificationCollectorMonitorService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
@ -86,11 +88,18 @@ public class GBApplication extends Application {
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
private static GBApplication app;
private static Logging logging = new Logging() {
@Override
protected String createLogDirectory() throws IOException {
File dir = FileUtils.getExternalFilesDir();
return dir.getAbsolutePath();
if (GBEnvironment.env().isLocalTest()) {
return System.getProperty(Logging.PROP_LOGFILES_DIR);
} else {
File dir = FileUtils.getExternalFilesDir();
return dir.getAbsolutePath();
}
}
};
@ -108,12 +117,17 @@ public class GBApplication extends Application {
// don't do anything here, add it to onCreate instead
}
public static Logging getLogging() {
return logging;
}
protected DeviceService createDeviceService() {
return new GBDeviceService(this);
}
@Override
public void onCreate() {
app = this;
super.onCreate();
if (lockHandler != null) {
@ -125,6 +139,13 @@ public class GBApplication extends Application {
prefs = new Prefs(sharedPrefs);
gbPrefs = new GBPrefs(prefs);
if (!GBEnvironment.isEnvironmentSetup()) {
GBEnvironment.setupEnvironment(GBEnvironment.createDeviceEnvironment());
// setup db after the environment is set up, but don't do it in test mode
// in test mode, it's done individually, see TestBase
setupDatabase();
}
// don't do anything here before we set up logging, otherwise
// slf4j may be implicitly initialized before we properly configured it.
setupLogging(isFileLoggingEnabled());
@ -135,10 +156,6 @@ public class GBApplication extends Application {
setupExceptionHandler();
GB.environment = GBEnvironment.createDeviceEnvironment();
setupDatabase(this);
deviceManager = new DeviceManager(this);
deviceService = createDeviceService();
@ -146,6 +163,8 @@ public class GBApplication extends Application {
if (isRunningMarshmallowOrLater()) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
//the following will ensure the notification manager is kept alive
startService(new Intent(this, NotificationCollectorMonitorService.class));
}
}
@ -191,8 +210,14 @@ public class GBApplication extends Application {
return prefs.getBoolean("minimize_priority", false);
}
static void setupDatabase(Context context) {
DBOpenHelper helper = new DBOpenHelper(context, DATABASE_NAME, null);
public void setupDatabase() {
DaoMaster.OpenHelper helper;
GBEnvironment env = GBEnvironment.env();
if (env.isTest()) {
helper = new DaoMaster.DevOpenHelper(this, null, null);
} else {
helper = new DBOpenHelper(this, DATABASE_NAME, null);
}
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
if (lockHandler == null) {
@ -310,10 +335,23 @@ public class GBApplication extends Application {
return NotificationManager.INTERRUPTION_FILTER_ALL;
}
public static HashSet<String> blacklist = null;
private static HashSet<String> blacklist = null;
public static boolean isBlacklisted(String packageName) {
return blacklist != null && blacklist.contains(packageName);
}
public static void setBlackList(Set<String> packageNames) {
if (packageNames == null) {
blacklist = new HashSet<>();
} else {
blacklist = new HashSet<>(packageNames);
}
saveBlackList();
}
private static void loadBlackList() {
blacklist = (HashSet<String>) sharedPrefs.getStringSet("package_blacklist", null);
blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (blacklist == null) {
blacklist = new HashSet<>();
}
@ -322,16 +360,15 @@ public class GBApplication extends Application {
private static void saveBlackList() {
SharedPreferences.Editor editor = sharedPrefs.edit();
if (blacklist.isEmpty()) {
editor.putStringSet("package_blacklist", null);
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
} else {
editor.putStringSet("package_blacklist", blacklist);
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, blacklist);
}
editor.apply();
}
public static void addToBlacklist(String packageName) {
if (!blacklist.contains(packageName)) {
blacklist.add(packageName);
if (blacklist.add(packageName)) {
saveBlackList();
}
}
@ -433,7 +470,7 @@ public class GBApplication extends Application {
public static int getTextColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true);
theme.resolveAttribute(R.attr.textColorPrimary, typedValue, true);
return typedValue.data;
}
@ -455,4 +492,8 @@ public class GBApplication extends Application {
public DeviceManager getDeviceManager() {
return deviceManager;
}
public static GBApplication app() {
return app;
}
}

View File

@ -20,6 +20,10 @@ package nodomain.freeyourgadget.gadgetbridge;
* Some more or less useful utility methods to aid local (non-device) testing.
*/
public class GBEnvironment {
// DO NOT USE A LOGGER HERE. Will break LoggingTest!
// private static final Logger LOG = LoggerFactory.getLogger(GBEnvironment.class);
private static GBEnvironment environment;
private boolean localTest;
private boolean deviceTest;
@ -41,4 +45,15 @@ public class GBEnvironment {
return localTest;
}
public static synchronized GBEnvironment env() {
return environment;
}
static synchronized boolean isEnvironmentSetup() {
return environment != null;
}
public synchronized static void setupEnvironment(GBEnvironment env) {
environment = env;
}
}

View File

@ -20,7 +20,6 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@ -36,7 +35,7 @@ public class LockHandler implements DBHandler {
public LockHandler() {
}
public void init(DaoMaster daoMaster, DBOpenHelper helper) {
public void init(DaoMaster daoMaster, DaoMaster.OpenHelper helper) {
if (isValid()) {
throw new IllegalStateException("DB must be closed before initializing it again");
}
@ -82,7 +81,7 @@ public class LockHandler implements DBHandler {
throw new IllegalStateException("session must be null");
}
// this will create completely new db instances and in turn update this handler through #init()
GBApplication.setupDatabase(GBApplication.getContext());
GBApplication.app().setupDatabase();
}
@Override

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, walkjivefly
This file is part of Gadgetbridge.
@ -18,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
/**
* Abstract base class for fragments. Provides hooks that are called when
@ -28,7 +28,7 @@ import android.support.v4.app.FragmentActivity;
* @see AbstractGBFragmentActivity
*/
public abstract class AbstractGBFragment extends Fragment {
private boolean mVisibleInactivity;
private boolean mVisibleInActivity;
/**
* Called when this fragment has been fully scrolled into the activity.
@ -37,7 +37,6 @@ public abstract class AbstractGBFragment extends Fragment {
* @see #onMadeInvisibleInActivity()
*/
protected void onMadeVisibleInActivity() {
updateActivityTitle();
}
/**
@ -47,7 +46,7 @@ public abstract class AbstractGBFragment extends Fragment {
* @see #onMadeVisibleInActivity()
*/
protected void onMadeInvisibleInActivity() {
mVisibleInactivity = false;
mVisibleInActivity = false;
}
/**
@ -55,16 +54,7 @@ public abstract class AbstractGBFragment extends Fragment {
* activity, not taking into account whether the screen is enabled at all.
*/
public boolean isVisibleInActivity() {
return mVisibleInactivity;
}
protected void updateActivityTitle() {
FragmentActivity activity = getActivity();
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
if (getTitle() != null) {
activity.setTitle(getTitle());
}
}
return mVisibleInActivity;
}
@Nullable
@ -76,7 +66,7 @@ public abstract class AbstractGBFragment extends Fragment {
* @hide
*/
public void onMadeVisibleInActivityInternal() {
mVisibleInactivity = true;
mVisibleInActivity = true;
if (isVisible()) {
onMadeVisibleInActivity();
}

View File

@ -21,8 +21,7 @@ import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.CheckedTextView;
import android.widget.TimePicker;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -36,16 +35,15 @@ public class AlarmDetails extends GBActivity {
private GBAlarm alarm;
private TimePicker timePicker;
private CheckBox cbSmartWakeup;
private CheckBox cbMonday;
private CheckBox cbTuesday;
private CheckBox cbWednesday;
private CheckBox cbThursday;
private CheckBox cbFriday;
private CheckBox cbSaturday;
private CheckBox cbSunday;
private CheckedTextView cbSmartWakeup;
private CheckedTextView cbMonday;
private CheckedTextView cbTuesday;
private CheckedTextView cbWednesday;
private CheckedTextView cbThursday;
private CheckedTextView cbFriday;
private CheckedTextView cbSaturday;
private CheckedTextView cbSunday;
private GBDevice device;
private TextView smartAlarmLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -56,15 +54,56 @@ public class AlarmDetails extends GBActivity {
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
timePicker = (TimePicker) findViewById(R.id.alarm_time_picker);
smartAlarmLabel = (TextView) findViewById(R.id.alarm_label_smart_wakeup);
cbSmartWakeup = (CheckBox) findViewById(R.id.alarm_cb_smart_wakeup);
cbMonday = (CheckBox) findViewById(R.id.alarm_cb_mon);
cbTuesday = (CheckBox) findViewById(R.id.alarm_cb_tue);
cbWednesday = (CheckBox) findViewById(R.id.alarm_cb_wed);
cbThursday = (CheckBox) findViewById(R.id.alarm_cb_thu);
cbFriday = (CheckBox) findViewById(R.id.alarm_cb_fri);
cbSaturday = (CheckBox) findViewById(R.id.alarm_cb_sat);
cbSunday = (CheckBox) findViewById(R.id.alarm_cb_sun);
cbSmartWakeup = (CheckedTextView) findViewById(R.id.alarm_cb_smart_wakeup);
cbMonday = (CheckedTextView) findViewById(R.id.alarm_cb_monday);
cbTuesday = (CheckedTextView) findViewById(R.id.alarm_cb_tuesday);
cbWednesday = (CheckedTextView) findViewById(R.id.alarm_cb_wednesday);
cbThursday = (CheckedTextView) findViewById(R.id.alarm_cb_thursday);
cbFriday = (CheckedTextView) findViewById(R.id.alarm_cb_friday);
cbSaturday = (CheckedTextView) findViewById(R.id.alarm_cb_saturday);
cbSunday = (CheckedTextView) findViewById(R.id.alarm_cb_sunday);
cbSmartWakeup.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbMonday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbTuesday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbWednesday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbThursday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbFriday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbSaturday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
cbSunday.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((CheckedTextView) v).toggle();
}
});
timePicker.setIs24HourView(DateFormat.is24HourFormat(GBApplication.getContext()));
timePicker.setCurrentHour(alarm.getHour());
@ -73,7 +112,6 @@ public class AlarmDetails extends GBActivity {
cbSmartWakeup.setChecked(alarm.isSmartWakeup());
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
cbSmartWakeup.setVisibility(smartAlarmVisibility);
smartAlarmLabel.setVisibility(smartAlarmVisibility);
cbMonday.setChecked(alarm.getRepetition(GBAlarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_TUE));

View File

@ -21,37 +21,27 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.view.LayoutInflater;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
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.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
public class AppBlacklistActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(AppBlacklistActivity.class);
private AppBlacklistAdapter appBlacklistAdapter;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -62,83 +52,35 @@ public class AppBlacklistActivity extends GBActivity {
}
};
private IdentityHashMap<ApplicationInfo, String> nameMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_appblacklist);
RecyclerView appListView = (RecyclerView) findViewById(R.id.appListView);
appListView.setLayoutManager(new LinearLayoutManager(this));
final PackageManager pm = getPackageManager();
appBlacklistAdapter = new AppBlacklistAdapter(R.layout.item_app_blacklist, this);
final List<ApplicationInfo> packageList = pm.getInstalledApplications(PackageManager.GET_META_DATA);
ListView appListView = (ListView) findViewById(R.id.appListView);
appListView.setAdapter(appBlacklistAdapter);
// sort the package list by label and blacklist status
nameMap = new IdentityHashMap<>(packageList.size());
for (ApplicationInfo ai : packageList) {
CharSequence name = pm.getApplicationLabel(ai);
if (name == null) {
name = ai.packageName;
}
if (GBApplication.blacklist.contains(ai.packageName)) {
// sort blacklisted first by prefixing with a '!'
name = "!" + name;
}
nameMap.put(ai, name.toString());
}
Collections.sort(packageList, new Comparator<ApplicationInfo>() {
SearchView searchView = (SearchView) findViewById(R.id.appListViewSearch);
searchView.setIconifiedByDefault(false);
searchView.setIconified(false);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public int compare(ApplicationInfo ai1, ApplicationInfo ai2) {
final String s1 = nameMap.get(ai1);
final String s2 = nameMap.get(ai2);
return s1.compareTo(s2);
public boolean onQueryTextSubmit(String query) {
return false;
}
});
final ArrayAdapter<ApplicationInfo> adapter = new ArrayAdapter<ApplicationInfo>(this, R.layout.item_with_checkbox, packageList) {
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item_with_checkbox, parent, false);
}
ApplicationInfo appInfo = packageList.get(position);
TextView deviceAppVersionAuthorLabel = (TextView) view.findViewById(R.id.item_details);
TextView deviceAppNameLabel = (TextView) view.findViewById(R.id.item_name);
ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_image);
CheckBox checkbox = (CheckBox) view.findViewById(R.id.item_checkbox);
deviceAppVersionAuthorLabel.setText(appInfo.packageName);
deviceAppNameLabel.setText(nameMap.get(appInfo));
deviceImageView.setImageDrawable(appInfo.loadIcon(pm));
checkbox.setChecked(GBApplication.blacklist.contains(appInfo.packageName));
return view;
}
};
appListView.setAdapter(adapter);
appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View v, int position, long id) {
String packageName = packageList.get(position).packageName;
CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox));
checkBox.toggle();
if (checkBox.isChecked()) {
GBApplication.addToBlacklist(packageName);
} else {
GBApplication.removeFromBlacklist(packageName);
}
public boolean onQueryTextChange(String newText) {
appBlacklistAdapter.getFilter().filter(newText);
return true;
}
});
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
}

View File

@ -19,8 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.MenuItem;
import android.widget.ListView;
import java.util.Arrays;
import java.util.HashSet;
@ -64,8 +65,10 @@ public class ConfigureAlarms extends GBActivity {
mGBAlarmListAdapter = new GBAlarmListAdapter(this, preferencesAlarmListSet);
ListView listView = (ListView) findViewById(R.id.alarm_list);
listView.setAdapter(mGBAlarmListAdapter);
RecyclerView alarmsRecyclerView = (RecyclerView) findViewById(R.id.alarm_list);
alarmsRecyclerView.setHasFixedSize(true);
alarmsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
alarmsRecyclerView.setAdapter(mGBAlarmListAdapter);
updateAlarmsFromPrefs();
}

View File

@ -250,15 +250,6 @@ public class ControlCenterv2 extends AppCompatActivity
private void refreshPairedDevices() {
List<GBDevice> deviceList = deviceManager.getDevices();
GBDevice connectedDevice = null;
for (GBDevice device : deviceList) {
if (device.isConnected() || device.isConnecting()) {
connectedDevice = device;
break;
}
}
if (deviceList.isEmpty()) {
background.setVisibility(View.VISIBLE);
} else {

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti
This file is part of Gadgetbridge.
@ -20,8 +20,10 @@ 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;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import android.view.View;
@ -33,6 +35,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -40,10 +43,13 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.ImportExportSharedPreferences;
public class DbManagementActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(DbManagementActivity.class);
private static SharedPreferences sharedPrefs;
private ImportExportSharedPreferences shared_file = new ImportExportSharedPreferences();
private Button exportDBButton;
private Button importDBButton;
@ -95,6 +101,8 @@ public class DbManagementActivity extends GBActivity {
deleteActivityDatabase();
}
});
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
}
private boolean hasOldActivityDatabase() {
@ -110,8 +118,33 @@ public class DbManagementActivity extends GBActivity {
return getString(R.string.dbmanagementactivvity_cannot_access_export_path);
}
private void exportShared() {
// BEGIN EXAMPLE
File myPath = null;
try {
myPath = FileUtils.getExternalFilesDir();
File myFile = new File(myPath, "Export_preference");
shared_file.exportToFile(sharedPrefs,myFile,null);
} catch (IOException ex) {
GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_shared, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
private void importShared() {
// BEGIN EXAMPLE
File myPath = null;
try {
myPath = FileUtils.getExternalFilesDir();
File myFile = new File(myPath, "Export_preference");
shared_file.importFromFile(sharedPrefs,myFile );
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
private void exportDB() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
exportShared();
DBHelper helper = new DBHelper(this);
File dir = FileUtils.getExternalFilesDir();
File destFile = helper.exportDB(dbHandler, dir);
@ -130,6 +163,7 @@ public class DbManagementActivity extends GBActivity {
@Override
public void onClick(DialogInterface dialog, int which) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
importShared();
DBHelper helper = new DBHelper(DbManagementActivity.this);
File dir = FileUtils.getExternalFilesDir();
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();

View File

@ -20,6 +20,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
@ -30,6 +31,7 @@ import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
@ -123,7 +125,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
}
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null && device.getAddress().equals(bondingAddress)) {
if (device != null && bondingDevice != null && device.getAddress().equals(bondingDevice.getMacAddress())) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_BONDED) {
handleDeviceBonded();
@ -134,11 +136,57 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
}
};
private void handleDeviceBonded() {
GB.toast(DiscoveryActivity.this, "Successfully bonded with: " + bondingAddress, Toast.LENGTH_SHORT, GB.INFO);
private void connectAndFinish(GBDevice device) {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_trying_to_connect_to, device.getName()), Toast.LENGTH_SHORT, GB.INFO);
GBApplication.deviceService().connect(device, true);
finish();
}
private void createBond(final GBDeviceCandidate deviceCandidate, int bondingStyle) {
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
return;
}
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_ASK) {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(DiscoveryActivity.this.getString(R.string.discovery_pair_title, deviceCandidate.getName()))
.setMessage(DiscoveryActivity.this.getString(R.string.discovery_pair_question))
.setPositiveButton(DiscoveryActivity.this.getString(R.string.discovery_yes_pair), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
doCreatePair(deviceCandidate);
}
})
.setNegativeButton(R.string.discovery_dont_pair, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
connectAndFinish(device);
}
})
.show();
} else {
doCreatePair(deviceCandidate);
}
}
private void doCreatePair(GBDeviceCandidate deviceCandidate) {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_attempting_to_pair, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.INFO);
if (deviceCandidate.getDevice().createBond()) {
// async, wait for bonding event to finish this activity
LOG.info("Bonding in progress...");
bondingDevice = deviceCandidate;
} else {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_bonding_failed_immediately, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.ERROR);
}
}
private void handleDeviceBonded() {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_successfully_bonded, bondingDevice.getName()), Toast.LENGTH_SHORT, GB.INFO);
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(bondingDevice);
connectAndFinish(device);
}
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
@ -203,7 +251,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
private DeviceCandidateAdapter cadidateListAdapter;
private Button startButton;
private Scanning isScanning = Scanning.SCANNING_OFF;
private String bondingAddress;
private GBDeviceCandidate bondingDevice;
private enum Scanning {
SCANNING_BT,
@ -358,7 +406,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
}
} else {
discoveryFinished();
Toast.makeText(this, "Enable Bluetooth to discover devices.", Toast.LENGTH_LONG).show();
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
}
}
@ -535,19 +583,24 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
startActivity(intent);
} else {
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
int bondingStyle = coordinator.getBondingStyle(device);
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
LOG.info("No bonding needed, according to coordinator, so connecting right away");
connectAndFinish(device);
return;
}
try {
BluetoothDevice btDevice = adapter.getRemoteDevice(deviceCandidate.getMacAddress());
switch (btDevice.getBondState()) {
case BluetoothDevice.BOND_NONE: {
if (btDevice.createBond()) {
// async, wait for bonding event to finish this activity
bondingAddress = btDevice.getAddress();
}
createBond(deviceCandidate, bondingStyle);
break;
}
case BluetoothDevice.BOND_BONDING:
// async, wait for bonding event to finish this activity
bondingAddress = btDevice.getAddress();
bondingDevice = deviceCandidate;
break;
case BluetoothDevice.BOND_BONDED:
handleDeviceBonded();

View File

@ -24,6 +24,7 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.LinearLayoutManager;
@ -259,11 +260,23 @@ public abstract class AbstractAppManagerFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final FloatingActionButton appListFab = ((FloatingActionButton) getActivity().findViewById(R.id.fab));
View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
RecyclerView appListView = (RecyclerView) (rootView.findViewById(R.id.appListView));
appListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) {
appListFab.hide();
} else if (dy < 0) {
appListFab.show();
}
}
});
appListView.setLayoutManager(new LinearLayoutManager(getActivity()));
mGBDeviceAppAdapter = new GBDeviceAppAdapter(appList, R.layout.item_with_details_and_drag_handle, this);
mGBDeviceAppAdapter = new GBDeviceAppAdapter(appList, R.layout.item_pebble_watchapp, this);
appListView.setAdapter(mGBDeviceAppAdapter);
ItemTouchHelper.Callback appItemTouchHelperCallback = new AppItemTouchHelperCallback(mGBDeviceAppAdapter);

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Daniele Gobbetti
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
@ -176,14 +177,11 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
static synchronized void rewriteAppOrderFile(String filename, List<UUID> uuids) {
try {
FileWriter fileWriter = new FileWriter(FileUtils.getExternalFilesDir() + "/" + filename);
BufferedWriter out = new BufferedWriter(fileWriter);
try (BufferedWriter out = new BufferedWriter(new FileWriter(FileUtils.getExternalFilesDir() + "/" + filename))) {
for (UUID uuid : uuids) {
out.write(uuid.toString());
out.newLine();
}
out.close();
} catch (IOException e) {
LOG.warn("can't write app order to file!");
}
@ -199,9 +197,7 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
static synchronized ArrayList<UUID> getUuidsFromFile(String filename) {
ArrayList<UUID> uuids = new ArrayList<>();
try {
FileReader fileReader = new FileReader(FileUtils.getExternalFilesDir() + "/" + filename);
BufferedReader in = new BufferedReader(fileReader);
try (BufferedReader in = new BufferedReader(new FileReader(FileUtils.getExternalFilesDir() + "/" + filename))) {
String line;
while ((line = in.readLine()) != null) {
uuids.add(UUID.fromString(line));

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti
Daniele Gobbetti, walkjivefly
This file is part of Gadgetbridge.
@ -93,7 +93,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
* shift the date by one day.
*/
public abstract class AbstractChartFragment extends AbstractGBFragment {
protected final int ANIM_TIME = 350;
protected final int ANIM_TIME = 250;
private static final Logger LOG = LoggerFactory.getLogger(AbstractChartFragment.class);
@ -154,10 +154,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
mIntentFilterActions = new HashSet<>();
if (intentFilterActions != null) {
mIntentFilterActions.addAll(Arrays.asList(intentFilterActions));
mIntentFilterActions.add(ChartsHost.DATE_NEXT);
mIntentFilterActions.add(ChartsHost.DATE_PREV);
mIntentFilterActions.add(ChartsHost.REFRESH);
}
mIntentFilterActions.add(ChartsHost.DATE_NEXT);
mIntentFilterActions.add(ChartsHost.DATE_PREV);
mIntentFilterActions.add(ChartsHost.REFRESH);
}
@Override
@ -182,9 +182,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
HEARTRATE_FILL_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_fill);
getContext().getTheme().resolveAttribute(R.attr.chart_activity, runningColor, true);
AK_ACTIVITY_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_light_sleep, runningColor, true);
AK_DEEP_SLEEP_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_deep_sleep, runningColor, true);
AK_DEEP_SLEEP_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_light_sleep, runningColor, true);
AK_LIGHT_SLEEP_COLOR = runningColor.data;
getContext().getTheme().resolveAttribute(R.attr.chart_not_worn, runningColor, true);
AK_NOT_WORN_COLOR = runningColor.data;
@ -234,7 +234,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
protected void showDateBar(boolean show) {
getChartsHost().getDateBar().setVisibility(show ? View.VISIBLE : View.GONE);
getChartsHost().getDateBar().setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}
@Override

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
/* Copyright (C) 2015-2017 0nse, Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti
This file is part of Gadgetbridge.
@ -25,7 +25,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
@ -81,13 +80,12 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
// setupLegend(mWeekChart);
setupLegend(mWeekChart);
mTodayPieChart.setCenterText(mcd.getDayData().centerText);
mTodayPieChart.setData(mcd.getDayData().data);
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mWeekChart.setData(mcd.getWeekBeforeData().getData());
mWeekChart.getLegend().setEnabled(false);
mWeekChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter());
}
@ -117,6 +115,7 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
BarData barData = new BarData(set);
barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
barData.setValueTextSize(10f);
LimitLine target = new LimitLine(mTargetValue);
barChart.getAxisLeft().removeAllLimitLines();
@ -133,23 +132,35 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
float totalValues[] = getTotalsForActivityAmounts(amounts);
String[] pieLabels = getPieLabels();
float totalValue = 0;
for (float value : totalValues) {
for (int i = 0; i < totalValues.length; i++) {
float value = totalValues[i];
totalValue += value;
entries.add(new PieEntry(value));
entries.add(new PieEntry(value, pieLabels[i]));
}
set.setValueFormatter(getPieValueFormatter());
set.setColors(getColors());
if (totalValue < mTargetValue) {
entries.add(new PieEntry((mTargetValue - totalValue)));
set.addColor(Color.GRAY);
if (totalValues.length < 2) {
if (totalValue < mTargetValue) {
entries.add(new PieEntry((mTargetValue - totalValue)));
set.addColor(Color.GRAY);
}
}
data.setDataSet(set);
//this hides the values (numeric) added to the set. These would be shown aside the strings set with addXValue above
data.setDrawValues(false);
if (totalValues.length < 2) {
data.setDrawValues(false);
}
else {
set.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
set.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
set.setValueTextColor(DESCRIPTION_COLOR);
set.setValueTextSize(13f);
set.setValueFormatter(getPieValueFormatter());
}
return new DayData(data, formatPieValue((int) totalValue));
}
@ -181,11 +192,11 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
private void setupTodayPieChart() {
mTodayPieChart.setBackgroundColor(BACKGROUND_COLOR);
mTodayPieChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mTodayPieChart.setEntryLabelColor(DESCRIPTION_COLOR);
mTodayPieChart.getDescription().setText(getPieDescription(mTargetValue));
// mTodayPieChart.setNoDataTextDescription("");
mTodayPieChart.setNoDataText("");
mTodayPieChart.getLegend().setEnabled(false);
// setupLegend(mTodayPieChart);
}
private void setupWeekChart() {
@ -222,16 +233,6 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
@Override
protected void setupLegend(Chart chart) {
// List<Integer> legendColors = new ArrayList<>(1);
// List<String> legendLabels = new ArrayList<>(1);
// legendColors.add(akActivity.color);
// legendLabels.add(getContext().getString(R.string.chart_steps));
// chart.getLegend().setCustom(legendColors, legendLabels);
// chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) {
int startTs;
int endTs;
@ -312,6 +313,8 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
abstract String formatPieValue(int value);
abstract String[] getPieLabels();
abstract IValueFormatter getPieValueFormatter();
abstract IValueFormatter getBarValueFormatter();

View File

@ -27,7 +27,6 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.PagerTabStrip;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.Menu;
@ -67,7 +66,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
private Date mStartDate;
private Date mEndDate;
private SwipeRefreshLayout swipeLayout;
private PagerTabStrip mPagerTabStrip;
private ViewPager viewPager;
LimitedQueue mActivityAmountCache = new LimitedQueue(60);
@ -200,7 +198,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
handleNextButtonClicked();
}
});
mPagerTabStrip = (PagerTabStrip) findViewById(R.id.charts_pagerTabStrip);
LinearLayout mainLayout = (LinearLayout) findViewById(R.id.charts_main_layout);
}

View File

@ -105,6 +105,10 @@ public class SleepChartFragment extends AbstractChartFragment {
}
});
set.setColors(colors);
set.setValueTextColor(DESCRIPTION_COLOR);
set.setValueTextSize(13f);
set.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
set.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
data.setDataSet(set);
//setupLegend(pieChart);
@ -162,6 +166,7 @@ public class SleepChartFragment extends AbstractChartFragment {
private void setupSleepAmountChart() {
mSleepAmountChart.setBackgroundColor(BACKGROUND_COLOR);
mSleepAmountChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mSleepAmountChart.setEntryLabelColor(DESCRIPTION_COLOR);
mSleepAmountChart.getDescription().setText("");
// mSleepAmountChart.getDescription().setNoDataTextDescription("");
mSleepAmountChart.setNoDataText("");

View File

@ -16,12 +16,16 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -40,7 +44,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
String getPieDescription(int targetValue) {
return getString(R.string.weeksleepchart_today_sleep_description, DateTimeUtils.minutesToHHMM(targetValue));
return getString(R.string.weeksleepchart_today_sleep_description, DateTimeUtils.formatDurationHoursMinutes(targetValue, TimeUnit.MINUTES));
}
@Override
@ -72,6 +76,11 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.MINUTES);
}
@Override
String[] getPieLabels() {
return new String[]{getString(R.string.abstract_chart_fragment_kind_deep_sleep), getString(R.string.abstract_chart_fragment_kind_light_sleep)};
}
@Override
IValueFormatter getPieValueFormatter() {
return new IValueFormatter() {
@ -106,4 +115,22 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
int[] getColors() {
return new int[]{akDeepSleep.color, akLightSleep.color};
}
@Override
protected void setupLegend(Chart chart) {
List<LegendEntry> legendEntries = new ArrayList<>(2);
LegendEntry lightSleepEntry = new LegendEntry();
lightSleepEntry.label = akLightSleep.label;
lightSleepEntry.formColor = akLightSleep.color;
legendEntries.add(lightSleepEntry);
LegendEntry deepSleepEntry = new LegendEntry();
deepSleepEntry.label = akDeepSleep.label;
deepSleepEntry.formColor = akDeepSleep.color;
legendEntries.add(deepSleepEntry);
chart.getLegend().setCustom(legendEntries);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
}

View File

@ -17,6 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
@ -62,6 +63,11 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
return String.valueOf(value);
}
@Override
String[] getPieLabels() {
return new String[]{""};
}
@Override
IValueFormatter getPieValueFormatter() {
return null;
@ -81,4 +87,10 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
int[] getColors() {
return new int[]{akActivity.color};
}
@Override
protected void setupLegend(Chart chart) {
// no legend here, it is all about the steps here
chart.getLegend().setEnabled(false);
}
}

View File

@ -0,0 +1,189 @@
/* Copyright (C) 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.adapter;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapter.AppBLViewHolder> implements Filterable {
private List<ApplicationInfo> applicationInfoList;
private final int mLayoutId;
private final Context mContext;
private final PackageManager mPm;
private final IdentityHashMap<ApplicationInfo, String> mNameMap;
private ApplicationFilter applicationFilter;
public AppBlacklistAdapter(int layoutId, Context context) {
mLayoutId = layoutId;
mContext = context;
mPm = context.getPackageManager();
applicationInfoList = mPm.getInstalledApplications(PackageManager.GET_META_DATA);
// sort the package list by label and blacklist status
mNameMap = new IdentityHashMap<>(applicationInfoList.size());
for (ApplicationInfo ai : applicationInfoList) {
CharSequence name = mPm.getApplicationLabel(ai);
if (name == null) {
name = ai.packageName;
}
if (GBApplication.isBlacklisted(ai.packageName)) {
// sort blacklisted first by prefixing with a '!'
name = "!" + name;
}
mNameMap.put(ai, name.toString());
}
Collections.sort(applicationInfoList, new Comparator<ApplicationInfo>() {
@Override
public int compare(ApplicationInfo ai1, ApplicationInfo ai2) {
final String s1 = mNameMap.get(ai1);
final String s2 = mNameMap.get(ai2);
return s1.compareTo(s2);
}
});
}
@Override
public AppBlacklistAdapter.AppBLViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(mLayoutId, parent, false);
return new AppBLViewHolder(view);
}
@Override
public void onBindViewHolder(AppBlacklistAdapter.AppBLViewHolder holder, int position) {
final ApplicationInfo appInfo = applicationInfoList.get(position);
holder.deviceAppVersionAuthorLabel.setText(appInfo.packageName);
holder.deviceAppNameLabel.setText(mNameMap.get(appInfo));
holder.deviceImageView.setImageDrawable(appInfo.loadIcon(mPm));
holder.checkbox.setChecked(GBApplication.isBlacklisted(appInfo.packageName));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox));
checkBox.toggle();
if (checkBox.isChecked()) {
GBApplication.addToBlacklist(appInfo.packageName);
} else {
GBApplication.removeFromBlacklist(appInfo.packageName);
}
}
});
}
@Override
public int getItemCount() {
return applicationInfoList.size();
}
@Override
public Filter getFilter() {
if (applicationFilter == null)
applicationFilter = new ApplicationFilter(this, applicationInfoList);
return applicationFilter;
}
public class AppBLViewHolder extends RecyclerView.ViewHolder {
final CheckBox checkbox;
final ImageView deviceImageView;
final TextView deviceAppVersionAuthorLabel;
final TextView deviceAppNameLabel;
AppBLViewHolder(View itemView) {
super(itemView);
checkbox = (CheckBox) itemView.findViewById(R.id.item_checkbox);
deviceImageView = (ImageView) itemView.findViewById(R.id.item_image);
deviceAppVersionAuthorLabel = (TextView) itemView.findViewById(R.id.item_details);
deviceAppNameLabel = (TextView) itemView.findViewById(R.id.item_name);
}
}
private class ApplicationFilter extends Filter {
private final AppBlacklistAdapter adapter;
private final List<ApplicationInfo> originalList;
private final List<ApplicationInfo> filteredList;
private ApplicationFilter(AppBlacklistAdapter adapter, List<ApplicationInfo> originalList) {
super();
this.originalList = new ArrayList<>(originalList);
this.filteredList = new ArrayList<>();
this.adapter = adapter;
}
@Override
protected Filter.FilterResults performFiltering(CharSequence filter) {
filteredList.clear();
final Filter.FilterResults results = new Filter.FilterResults();
if (filter == null || filter.length() == 0)
filteredList.addAll(originalList);
else {
final String filterPattern = filter.toString().toLowerCase().trim();
for (ApplicationInfo ai : originalList) {
CharSequence name = mPm.getApplicationLabel(ai);
if (name.toString().contains(filterPattern) ||
(ai.packageName.contains(filterPattern))) {
filteredList.add(ai);
}
}
}
results.values = filteredList;
results.count = filteredList.size();
return results;
}
@Override
protected void publishResults(CharSequence charSequence, Filter.FilterResults filterResults) {
adapter.applicationInfoList.clear();
adapter.applicationInfoList.addAll((List<ApplicationInfo>) filterResults.values);
adapter.notifyDataSetChanged();
}
}
}

View File

@ -19,20 +19,21 @@ package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
import android.graphics.Color;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckedTextView;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
@ -41,22 +42,18 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
/**
* Adapter for displaying GBAlarm instances.
*/
public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.ViewHolder> {
private final Context mContext;
private ArrayList<GBAlarm> alarmList;
public GBAlarmListAdapter(Context context, ArrayList<GBAlarm> alarmList) {
super(context, 0, alarmList);
private List<GBAlarm> alarmList;
public GBAlarmListAdapter(Context context, List<GBAlarm> alarmList) {
this.mContext = context;
this.alarmList = alarmList;
}
public GBAlarmListAdapter(Context context, Set<String> preferencesAlarmListSet) {
super(context, 0, new ArrayList<GBAlarm>());
this.mContext = context;
alarmList = new ArrayList<>();
@ -81,7 +78,7 @@ public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
}
public ArrayList<? extends Alarm> getAlarmList() {
return alarmList;
return (ArrayList) alarmList;
}
@ -95,53 +92,26 @@ public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
}
@Override
public int getCount() {
if (alarmList != null) {
return alarmList.size();
}
return 0;
public GBAlarmListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_alarm, parent, false);
ViewHolder vh = new ViewHolder(view);
return vh;
}
@Override
public GBAlarm getItem(int position) {
if (alarmList != null) {
return alarmList.get(position);
}
return null;
}
public void onBindViewHolder(ViewHolder holder, final int position) {
@Override
public long getItemId(int position) {
if (alarmList != null) {
return alarmList.get(position).getIndex();
}
return 0;
}
final GBAlarm alarm = alarmList.get(position);
@Override
public View getView(int position, View view, ViewGroup parent) {
holder.alarmDayMonday.setChecked(alarm.getRepetition(Alarm.ALARM_MON));
holder.alarmDayTuesday.setChecked(alarm.getRepetition(Alarm.ALARM_TUE));
holder.alarmDayWednesday.setChecked(alarm.getRepetition(Alarm.ALARM_WED));
holder.alarmDayThursday.setChecked(alarm.getRepetition(Alarm.ALARM_THU));
holder.alarmDayFriday.setChecked(alarm.getRepetition(Alarm.ALARM_FRI));
holder.alarmDaySaturday.setChecked(alarm.getRepetition(Alarm.ALARM_SAT));
holder.alarmDaySunday.setChecked(alarm.getRepetition(Alarm.ALARM_SUN));
final GBAlarm alarm = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.alarm_item, parent, false);
}
TextView alarmTime = (TextView) view.findViewById(R.id.alarm_item_time);
Switch isEnabled = (Switch) view.findViewById(R.id.alarm_item_toggle);
TextView isSmartWakeup = (TextView) view.findViewById(R.id.alarm_smart_wakeup);
highlightDay((TextView) view.findViewById(R.id.alarm_item_sunday), alarm.getRepetition(Alarm.ALARM_SUN));
highlightDay((TextView) view.findViewById(R.id.alarm_item_monday), alarm.getRepetition(Alarm.ALARM_MON));
highlightDay((TextView) view.findViewById(R.id.alarm_item_tuesday), alarm.getRepetition(Alarm.ALARM_TUE));
highlightDay((TextView) view.findViewById(R.id.alarm_item_wednesday), alarm.getRepetition(Alarm.ALARM_WED));
highlightDay((TextView) view.findViewById(R.id.alarm_item_thursday), alarm.getRepetition(Alarm.ALARM_THU));
highlightDay((TextView) view.findViewById(R.id.alarm_item_friday), alarm.getRepetition(Alarm.ALARM_FRI));
highlightDay((TextView) view.findViewById(R.id.alarm_item_saturday), alarm.getRepetition(Alarm.ALARM_SAT));
isEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
holder.isEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
alarm.setEnabled(isChecked);
@ -149,28 +119,62 @@ public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
}
});
view.setOnClickListener(new View.OnClickListener() {
holder.container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((ConfigureAlarms) mContext).configureAlarm(alarm);
}
});
alarmTime.setText(alarm.getTime());
isEnabled.setChecked(alarm.isEnabled());
holder.alarmTime.setText(alarm.getTime());
holder.isEnabled.setChecked(alarm.isEnabled());
if (alarm.isSmartWakeup()) {
isSmartWakeup.setVisibility(TextView.VISIBLE);
holder.isSmartWakeup.setVisibility(TextView.VISIBLE);
} else {
isSmartWakeup.setVisibility(TextView.GONE);
}
return view;
}
private void highlightDay(TextView view, boolean isOn) {
if (isOn) {
view.setTextColor(Color.BLUE);
} else {
view.setTextColor(GBApplication.getTextColor(mContext));
holder.isSmartWakeup.setVisibility(TextView.GONE);
}
}
@Override
public int getItemCount() {
return alarmList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
CardView container;
TextView alarmTime;
Switch isEnabled;
TextView isSmartWakeup;
CheckedTextView alarmDayMonday;
CheckedTextView alarmDayTuesday;
CheckedTextView alarmDayWednesday;
CheckedTextView alarmDayThursday;
CheckedTextView alarmDayFriday;
CheckedTextView alarmDaySaturday;
CheckedTextView alarmDaySunday;
ViewHolder(View view) {
super(view);
container = (CardView) view.findViewById(R.id.card_view);
alarmTime = (TextView) view.findViewById(R.id.alarm_item_time);
isEnabled = (Switch) view.findViewById(R.id.alarm_item_toggle);
isSmartWakeup = (TextView) view.findViewById(R.id.alarm_smart_wakeup);
alarmDayMonday = (CheckedTextView) view.findViewById(R.id.alarm_item_monday);
alarmDayTuesday = (CheckedTextView) view.findViewById(R.id.alarm_item_tuesday);
alarmDayWednesday = (CheckedTextView) view.findViewById(R.id.alarm_item_wednesday);
alarmDayThursday = (CheckedTextView) view.findViewById(R.id.alarm_item_thursday);
alarmDayFriday = (CheckedTextView) view.findViewById(R.id.alarm_item_friday);
alarmDaySaturday = (CheckedTextView) view.findViewById(R.id.alarm_item_saturday);
alarmDaySunday = (CheckedTextView) view.findViewById(R.id.alarm_item_sunday);
}
}
}

View File

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

View File

@ -384,7 +384,9 @@ public class DBHelper {
} else {
ensureDeviceUpToDate(device, gbDevice, session);
}
ensureDeviceAttributes(device, gbDevice, session);
if (gbDevice.isInitialized()) {
ensureDeviceAttributes(device, gbDevice, session);
}
return device;
}

View File

@ -119,4 +119,9 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
}
return false;
}
@Override
public int getBondingStyle(GBDevice device) {
return BONDING_STYLE_ASK;
}
}

View File

@ -23,7 +23,6 @@ import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -47,6 +46,21 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
*/
public interface DeviceCoordinator {
String EXTRA_DEVICE_CANDIDATE = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_CANDIDATE";
/**
* Do not attempt to bond after discovery.
*/
int BONDING_STYLE_NONE = 0;
/**
* Bond after discovery.
* This is not recommended, as there are mobile devices on which bonding does not work.
* Prefer to use #BONDING_STYLE_ASK instead.
*/
int BONDING_STYLE_BOND = 1;
/**
* Let the user decide whether to bond or not after discovery.
* Prefer this over #BONDING_STYLE_BOND
*/
int BONDING_STYLE_ASK = 2;
/**
* Checks whether this coordinator handles the given candidate.
@ -207,4 +221,17 @@ public interface DeviceCoordinator {
* @return
*/
Class<? extends Activity> getAppsManagementActivity();
/**
* Returns how/if the given device should be bonded before connecting to it.
* @param device
*/
int getBondingStyle(GBDevice device);
/**
* Indicates whether the device has some kind of calender we can sync to.
* Also used for generated sunrise/sunset events
*/
boolean supportsCalendarEvents();
}

View File

@ -34,6 +34,9 @@ import java.util.Comparator;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
@ -88,6 +91,12 @@ public class DeviceManager {
} else {
deviceList.add(dev);
}
if (dev.isInitialized()) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DBHelper.getDevice(dev, dbHandler.getDaoSession()); // implicitly creates the device in database if not present, and updates device attributes
} catch (Exception ignore) {
}
}
}
updateSelectedDevice(dev);
refreshPairedDevices();

View File

@ -176,4 +176,9 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 João Paulo Barraca
/* Copyright (C) 2016-2017 Carsten Pfeiffer, João Paulo Barraca
This file is part of Gadgetbridge.
@ -121,6 +121,9 @@ public final class HPlusConstants {
public static final byte DATA_DAY_SUMMARY_ALT = 0x39;
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_UNKNOWN = 0x4d;
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";
public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr";
@ -129,61 +132,63 @@ public final class HPlusConstants {
public static final String PREF_HPLUS_WRIST = "hplus_wrist";
public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time";
public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time";
public static final String PREF_HPLUS_UNICODE = "hplus_unicode";
public static final Map<Character, Byte> transliterateMap = new HashMap<Character, Byte>(){
public static final Map<Character, byte[]> transliterateMap = new HashMap<Character, byte[]>(){
{
//These are missing
put('ó', new Byte((byte) 111));
put('Ó', new Byte((byte) 79));
put('í', new Byte((byte) 105));
put('Í', new Byte((byte) 73));
put('ú', new Byte((byte) 117));
put('Ú', new Byte((byte) 85));
put('ó', new byte[]{(byte) 111});
put('Ó', new byte[]{(byte) 79});
put('í', new byte[]{(byte) 105});
put('Í', new byte[]{(byte) 73});
put('ú', new byte[]{(byte) 117});
put('Ú', new byte[]{(byte) 85});
//These mostly belong to the extended ASCII table
put('Ç', new Byte((byte) 128));
put('ü', new Byte((byte) 129));
put('é', new Byte((byte) 130));
put('â', new Byte((byte) 131));
put('ä', new Byte((byte) 132));
put('à', new Byte((byte) 133));
put('ã', new Byte((byte) 134));
put('ç', new Byte((byte) 135));
put('ê', new Byte((byte) 136));
put('ë', new Byte((byte) 137));
put('è', new Byte((byte) 138));
put('Ï', new Byte((byte) 139));
put('Î', new Byte((byte) 140));
put('Ì', new Byte((byte) 141));
put('Ã', new Byte((byte) 142));
put('Ä', new Byte((byte) 143));
put('É', new Byte((byte) 144));
put('æ', new Byte((byte) 145));
put('Æ', new Byte((byte) 146));
put('ô', new Byte((byte) 147));
put('ö', new Byte((byte) 148));
put('ò', new Byte((byte) 149));
put('û', new Byte((byte) 150));
put('ù', new Byte((byte) 151));
put('ÿ', new Byte((byte) 152));
put('Ö', new Byte((byte) 153));
put('Ü', new Byte((byte) 154));
put('¢', new Byte((byte) 155));
put('£', new Byte((byte) 156));
put('¥', new Byte((byte) 157));
put('ƒ', new Byte((byte) 159));
put('á', new Byte((byte) 160));
put('ñ', new Byte((byte) 164));
put('Ñ', new Byte((byte) 165));
put('ª', new Byte((byte) 166));
put('º', new Byte((byte) 167));
put('¿', new Byte((byte) 168));
put('¬', new Byte((byte) 170));
put('½', new Byte((byte) 171));
put('¼', new Byte((byte) 172));
put('¡', new Byte((byte) 173));
put('«', new Byte((byte) 174));
put('»', new Byte((byte) 175));
put('Ç', new byte[]{(byte) 128});
put('ü', new byte[]{(byte) 129});
put('é', new byte[]{(byte) 130});
put('â', new byte[]{(byte) 131});
put('ä', new byte[]{(byte) 132});
put('à', new byte[]{(byte) 133});
put('ã', new byte[]{(byte) 134});
put('ç', new byte[]{(byte) 135});
put('ê', new byte[]{(byte) 136});
put('ë', new byte[]{(byte) 137});
put('Ï', new byte[]{(byte) 139});
put('è', new byte[]{(byte) 138});
put('Î', new byte[]{(byte) 140});
put('Ì', new byte[]{(byte) 141});
put('Ã', new byte[]{(byte) 142});
put('Ä', new byte[]{(byte) 143});
put('É', new byte[]{(byte) 144});
put('æ', new byte[]{(byte) 145});
put('Æ', new byte[]{(byte) 146});
put('ô', new byte[]{(byte) 147});
put('ö', new byte[]{(byte) 148});
put('ò', new byte[]{(byte) 149});
put('û', new byte[]{(byte) 150});
put('ù', new byte[]{(byte) 151});
put('ÿ', new byte[]{(byte) 152});
put('Ö', new byte[]{(byte) 153});
put('Ü', new byte[]{(byte) 154});
put('¢', new byte[]{(byte) 155});
put('£', new byte[]{(byte) 156});
put('¥', new byte[]{(byte) 157});
put('ƒ', new byte[]{(byte) 159});
put('á', new byte[]{(byte) 160});
put('ñ', new byte[]{(byte) 164});
put('Ñ', new byte[]{(byte) 165});
put('ª', new byte[]{(byte) 166});
put('º', new byte[]{(byte) 167});
put('¿', new byte[]{(byte) 168});
put('¬', new byte[]{(byte) 170});
put('½', new byte[]{(byte) 171});
put('¼', new byte[]{(byte) 172});
put('¡', new byte[]{(byte) 173});
put('«', new byte[]{(byte) 174});
put('»', new byte[]{(byte) 175});
put('°', new byte[]{(byte) 0xa1, (byte) 0xe3});
}
};
}

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, João Paulo Barraca
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, João
Paulo Barraca
This file is part of Gadgetbridge.
@ -24,6 +25,7 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
@ -80,6 +82,16 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return DeviceType.UNKNOWN;
}
@Override
public int getBondingStyle(GBDevice deviceCandidate){
return BONDING_STYLE_NONE;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HPLUS;
@ -187,7 +199,6 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}else{
return HPlusConstants.ARG_TIMEMODE_12H;
}
}
public static byte getUnit(String address) {
@ -200,25 +211,25 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}
}
public static byte getUserWeight(String address) {
public static byte getUserWeight() {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getWeightKg() & 0xFF);
}
public static byte getUserHeight(String address) {
public static byte getUserHeight() {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getHeightCm() & 0xFF);
}
public static byte getUserAge(String address) {
public static byte getUserAge() {
ActivityUser activityUser = new ActivityUser();
return (byte) (activityUser.getAge() & 0xFF);
}
public static byte getUserGender(String address) {
public static byte getUserGender() {
ActivityUser activityUser = new ActivityUser();
if (activityUser.getGender() == ActivityUser.GENDER_MALE)
@ -227,7 +238,7 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return HPlusConstants.ARG_GENDER_FEMALE;
}
public static int getGoal(String address) {
public static int getGoal() {
ActivityUser activityUser = new ActivityUser();
return activityUser.getStepsGoal();
@ -271,4 +282,13 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return prefs.getInt(HPlusConstants.PREF_HPLUS_SIT_END_TIME, 0);
}
public static void setUnicodeSupport(String address, boolean state){
SharedPreferences.Editor editor = prefs.getPreferences().edit();
editor.putBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, state);
editor.commit();
}
public static boolean getUnicodeSupport(String address){
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE, false));
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 João Paulo Barraca
/* Copyright (C) 2017 Andreas Shimokawa, João Paulo Barraca
This file is part of Gadgetbridge.

View File

@ -120,6 +120,11 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet

View File

@ -171,6 +171,11 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
public static boolean hasValidUserInfo() {
String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00";
try {

View File

@ -23,14 +23,24 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertLevel;
public class VibrationProfile {
public static final Context CONTEXT = GBApplication.getContext();
public static final String ID_STACCATO = CONTEXT.getString(R.string.p_staccato);
public static final String ID_SHORT = CONTEXT.getString(R.string.p_short);
public static final String ID_MEDIUM = CONTEXT.getString(R.string.p_medium);
public static final String ID_LONG = CONTEXT.getString(R.string.p_long);
public static final String ID_WATERDROP = CONTEXT.getString(R.string.p_waterdrop);
public static final String ID_RING = CONTEXT.getString(R.string.p_ring);
public static final String ID_ALARM_CLOCK = CONTEXT.getString(R.string.p_alarm_clock);
public static final String ID_STACCATO;
public static final String ID_SHORT;
public static final String ID_MEDIUM;
public static final String ID_LONG;
public static final String ID_WATERDROP;
public static final String ID_RING;
public static final String ID_ALARM_CLOCK;
static {
Context CONTEXT = GBApplication.getContext();
ID_STACCATO = CONTEXT.getString(R.string.p_staccato);
ID_SHORT = CONTEXT.getString(R.string.p_short);
ID_MEDIUM = CONTEXT.getString(R.string.p_medium);
ID_LONG = CONTEXT.getString(R.string.p_long);
ID_WATERDROP = CONTEXT.getString(R.string.p_waterdrop);
ID_RING = CONTEXT.getString(R.string.p_ring);
ID_ALARM_CLOCK = CONTEXT.getString(R.string.p_alarm_clock);
}
public static VibrationProfile getProfile(String id, short repeat) {
if (ID_STACCATO.equals(id)) {

View File

@ -20,6 +20,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -48,6 +49,7 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
public PebbleCoordinator() {
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
@ -73,7 +75,7 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
}
@Override
protected void deleteDevice(GBDevice gbDevice, Device device, DaoSession session) throws GBException {
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getPebbleHealthActivitySampleDao().queryBuilder();
qb.where(PebbleHealthActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
@ -159,4 +161,9 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
public Class<? extends Activity> getAppsManagementActivity() {
return AppManagerActivity.class;
}
@Override
public boolean supportsCalendarEvents() {
return true;
}
}

View File

@ -120,6 +120,11 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -28,7 +28,7 @@ public class AutoStartReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (GBApplication.getGBPrefs().getAutoStart()) {
if (GBApplication.getGBPrefs().getAutoStart() && Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.i(TAG, "Boot completed, starting Gadgetbridge");
GBApplication.deviceService().start();
}

View File

@ -0,0 +1,73 @@
/* Copyright (C) 2016-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.externalevents;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
/**
* Created by jpbarraca on 13/04/2017.
*/
public class BluetoothPairingRequestReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(BluetoothConnectReceiver.class);
final DeviceCommunicationService service;
public BluetoothPairingRequestReceiver(DeviceCommunicationService service) {
this.service = service;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (!action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) {
return;
}
GBDevice gbDevice = service.getGBDevice();
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (gbDevice == null || device == null)
return;
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
try {
if (coordinator.getBondingStyle(gbDevice) == DeviceCoordinator.BONDING_STYLE_NONE) {
LOG.info("Aborting unwanted pairing request");
abortBroadcast();
}
} catch (Exception e) {
LOG.warn("Could not abort pairing request process");
}
}
}

View File

@ -50,7 +50,7 @@ public class BluetoothStateChangeReceiver extends BroadcastReceiver {
LOG.info("Bluetooth turned on => connecting...");
GBApplication.deviceService().connect();
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {
GBApplication.quit();
GBApplication.deviceService().disconnect();
}
}
}

View File

@ -0,0 +1,216 @@
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti,
Daniel Hauck
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.externalevents;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Hashtable;
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;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncStateDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class CalendarReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(CalendarReceiver.class);
private Hashtable<Long, EventSyncState> eventState = new Hashtable<>();
private GBDevice mGBDevice;
private class EventSyncState {
private int state;
private CalendarEvents.CalendarEvent event;
EventSyncState(CalendarEvents.CalendarEvent event, int state) {
this.state = state;
this.event = event;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public CalendarEvents.CalendarEvent getEvent() {
return event;
}
public void setEvent(CalendarEvents.CalendarEvent event) {
this.event = event;
}
}
private static class EventState {
private static final int NOT_SYNCED = 0;
private static final int SYNCED = 1;
private static final int NEEDS_UPDATE = 2;
private static final int NEEDS_DELETE = 3;
}
public CalendarReceiver(GBDevice gbDevice) {
LOG.info("Created calendar receiver.");
mGBDevice = gbDevice;
onReceive(GBApplication.getContext(), new Intent());
}
@Override
public void onReceive(Context context, Intent intent) {
LOG.info("got calendar changed broadcast");
List<CalendarEvents.CalendarEvent> eventList = (new CalendarEvents()).getCalendarEventList(GBApplication.getContext());
syncCalendar(eventList);
}
public void syncCalendar(List<CalendarEvents.CalendarEvent> eventList) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
syncCalendar(eventList, session);
} catch (Exception e1) {
GB.toast("Database Error while syncing Calendar", Toast.LENGTH_SHORT, GB.ERROR);
}
}
public void syncCalendar(List<CalendarEvents.CalendarEvent> eventList, DaoSession session) {
LOG.info("Syncing with calendar.");
Hashtable<Long, CalendarEvents.CalendarEvent> eventTable = new Hashtable<>();
Long deviceId = DBHelper.getDevice(mGBDevice, session).getId();
QueryBuilder<CalendarSyncState> qb = session.getCalendarSyncStateDao().queryBuilder();
for (CalendarEvents.CalendarEvent e : eventList) {
long id = e.getId();
eventTable.put(id, e);
if (!eventState.containsKey(e.getId())) {
qb = session.getCalendarSyncStateDao().queryBuilder();
CalendarSyncState calendarSyncState = qb.where(qb.and(CalendarSyncStateDao.Properties.DeviceId.eq(deviceId), CalendarSyncStateDao.Properties.CalendarEntryId.eq(id)))
.build().unique();
if (calendarSyncState == null) {
eventState.put(id, new EventSyncState(e, EventState.NOT_SYNCED));
LOG.info("event id=" + id + " is yet unknown to device id=" + deviceId);
} else if (calendarSyncState.getHash() == e.hashCode()) {
eventState.put(id, new EventSyncState(e, EventState.SYNCED));
LOG.info("event id=" + id + " is up to date on device id=" + deviceId);
}
else {
eventState.put(id, new EventSyncState(e, EventState.NEEDS_UPDATE));
LOG.info("event id=" + id + " is not up to date on device id=" + deviceId);
}
}
}
// add all missing calendar ids on the device to sync status (so that they are deleted later)
List<CalendarSyncState> CalendarSyncStateList = qb.where(CalendarSyncStateDao.Properties.DeviceId.eq(deviceId)).build().list();
for (CalendarSyncState CalendarSyncState : CalendarSyncStateList) {
if (!eventState.containsKey(CalendarSyncState.getCalendarEntryId())) {
eventState.put(CalendarSyncState.getCalendarEntryId(), new EventSyncState(null, EventState.NEEDS_DELETE));
LOG.info("insert null event for orphanded calendar id=" + CalendarSyncState.getCalendarEntryId() + " for device=" + mGBDevice.getName());
}
}
Enumeration<Long> ids = eventState.keys();
while (ids.hasMoreElements()) {
qb = session.getCalendarSyncStateDao().queryBuilder();
Long i = ids.nextElement();
EventSyncState es = eventState.get(i);
if (eventTable.containsKey(i)) {
if (es.getState() == EventState.SYNCED) {
if (!es.getEvent().equals(eventTable.get(i))) {
eventState.put(i, new EventSyncState(eventTable.get(i), EventState.NEEDS_UPDATE));
}
}
} else {
if (es.getState() == EventState.NOT_SYNCED) {
// delete for current device only
qb.where(qb.and(CalendarSyncStateDao.Properties.DeviceId.eq(deviceId), CalendarSyncStateDao.Properties.CalendarEntryId.eq(i)))
.buildDelete().executeDeleteWithoutDetachingEntities();
eventState.remove(i);
} else {
es.setState(EventState.NEEDS_DELETE);
eventState.put(i, es);
}
}
updateEvents(deviceId, session);
}
}
private void updateEvents(Long deviceId, DaoSession session) {
Enumeration<Long> ids = eventState.keys();
while (ids.hasMoreElements()) {
Long i = ids.nextElement();
EventSyncState es = eventState.get(i);
int syncState = es.getState();
if (syncState == EventState.NOT_SYNCED || syncState == EventState.NEEDS_UPDATE) {
CalendarEvents.CalendarEvent calendarEvent = es.getEvent();
CalendarEventSpec calendarEventSpec = new CalendarEventSpec();
calendarEventSpec.id = i;
calendarEventSpec.title = calendarEvent.getTitle();
calendarEventSpec.allDay = calendarEvent.isAllDay();
calendarEventSpec.timestamp = calendarEvent.getBeginSeconds();
calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now
if (calendarEvent.isAllDay()) {
//force the all day events to begin at midnight and last a whole day
Calendar c = GregorianCalendar.getInstance();
c.setTimeInMillis(calendarEvent.getBegin());
c.set(Calendar.HOUR, 0);
calendarEventSpec.timestamp = (int) (c.getTimeInMillis() / 1000);
calendarEventSpec.durationInSeconds = 24 * 60 * 60;
}
calendarEventSpec.description = calendarEvent.getDescription();
calendarEventSpec.location = calendarEvent.getLocation();
calendarEventSpec.type = CalendarEventSpec.TYPE_UNKNOWN;
if (syncState == EventState.NEEDS_UPDATE) {
GBApplication.deviceService().onDeleteCalendarEvent(CalendarEventSpec.TYPE_UNKNOWN, i);
}
GBApplication.deviceService().onAddCalendarEvent(calendarEventSpec);
es.setState(EventState.SYNCED);
eventState.put(i, es);
// update db
session.insertOrReplace(new CalendarSyncState(null, deviceId, i, es.event.hashCode()));
} else if (syncState == EventState.NEEDS_DELETE) {
GBApplication.deviceService().onDeleteCalendarEvent(CalendarEventSpec.TYPE_UNKNOWN, i);
eventState.remove(i);
// delete from db for current device only
QueryBuilder<CalendarSyncState> qb = session.getCalendarSyncStateDao().queryBuilder();
qb.where(qb.and(CalendarSyncStateDao.Properties.DeviceId.eq(deviceId), CalendarSyncStateDao.Properties.CalendarEntryId.eq(i)))
.buildDelete().executeDeleteWithoutDetachingEntities();
}
}
}
}

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,20 @@ 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.Parcelable;
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 +78,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();
@ -89,7 +91,7 @@ public class NotificationListener extends NotificationListenerService {
StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications();
int handle = intent.getIntExtra("handle", -1);
for (StatusBarNotification sbn : sbns) {
if ((int) sbn.getPostTime() == handle) {
if ((sbn.getPackageName().hashCode() * 31 + sbn.getId()) == handle) {
if (action.equals(ACTION_OPEN)) {
try {
PendingIntent pi = sbn.getNotification().contentIntent;
@ -112,7 +114,7 @@ public class NotificationListener extends NotificationListenerService {
StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications();
int handle = intent.getIntExtra("handle", -1);
for (StatusBarNotification sbn : sbns) {
if ((int) sbn.getPostTime() == handle) {
if ((sbn.getPackageName().hashCode() * 31 + sbn.getId()) == handle) {
if (GBApplication.isRunningLollipopOrLater()) {
String key = sbn.getKey();
NotificationListener.this.cancelNotification(key);
@ -177,16 +179,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 +195,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.blacklist != null && GBApplication.blacklist.contains(source)) {
LOG.info("Not forwarding notification, application is blacklisted");
return;
}
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = source.hashCode() * 31 + sbn.getId();
// determinate Source App Name ("Label")
PackageManager pm = getPackageManager();
@ -261,26 +210,17 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.sourceName = (String) pm.getApplicationLabel(ai);
}
boolean preferBigText = false;
boolean preferBigText = true; //changed to true since now we update the former ID
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;
}
if (notificationSpec.type == null) {
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 +232,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 +241,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);
@ -320,10 +261,32 @@ public class NotificationListener extends NotificationListenerService {
}
CharSequence contentCS = null;
if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) {
contentCS = extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
} else if (extras.containsKey(Notification.EXTRA_TEXT)) {
contentCS = extras.getCharSequence(Notification.EXTRA_TEXT);
if (extras.containsKey(Notification.EXTRA_MESSAGES)) {
Parcelable[] parcelables = extras.getParcelableArray(NotificationCompat.EXTRA_MESSAGES);
String contentBuilder = "";
CharSequence sender;
CharSequence prevSender = "";
CharSequence message;
for (Parcelable p : parcelables) {
if (!(p instanceof Bundle))
continue;
sender = ((Bundle) p).getCharSequence("sender");
message = ((Bundle) p).getCharSequence("text");
if (sender == null || message == null)
continue;
if (!sender.equals(prevSender) && !sender.equals(notificationSpec.title)) {
contentBuilder += sender.toString() + ": ";
prevSender = sender;
}
contentBuilder += message.toString() + "\n";
}
contentCS = contentBuilder;
} else {
if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) {
contentCS = extras.getCharSequence(NotificationCompat.EXTRA_BIG_TEXT);
} else if (extras.containsKey(Notification.EXTRA_TEXT)) {
contentCS = extras.getCharSequence(NotificationCompat.EXTRA_TEXT);
}
}
if (contentCS != null) {
notificationSpec.body = contentCS.toString();
@ -344,31 +307,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,53 +338,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
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
if (shouldIgnore(sbn))
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());
}
}
@ -447,4 +385,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.isBlacklisted(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

@ -105,10 +105,10 @@ public class GBDeviceService implements DeviceService {
}
@Override
public void connect(@Nullable GBDevice device, boolean performPair) {
public void connect(@Nullable GBDevice device, boolean firstTime) {
Intent intent = createIntent().setAction(ACTION_CONNECT)
.putExtra(GBDevice.EXTRA_DEVICE, device)
.putExtra(EXTRA_PERFORM_PAIR, performPair);
.putExtra(EXTRA_CONNECT_FIRST_TIME, firstTime);
invokeService(intent);
}
@ -332,7 +332,8 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_CALENDAREVENT_TIMESTAMP, calendarEventSpec.timestamp)
.putExtra(EXTRA_CALENDAREVENT_DURATION, calendarEventSpec.durationInSeconds)
.putExtra(EXTRA_CALENDAREVENT_TITLE, calendarEventSpec.title)
.putExtra(EXTRA_CALENDAREVENT_DESCRIPTION, calendarEventSpec.description);
.putExtra(EXTRA_CALENDAREVENT_DESCRIPTION, calendarEventSpec.description)
.putExtra(EXTRA_CALENDAREVENT_LOCATION, calendarEventSpec.location);
invokeService(intent);
}

View File

@ -44,6 +44,9 @@ public class AppNotificationType extends HashMap<String, NotificationType> {
put("com.sonyericsson.conversations", NotificationType.GENERIC_SMS);
put("org.smssecure.smssecure", NotificationType.GENERIC_SMS);
// Generic Calendar
put("com.android.calendar", NotificationType.GENERIC_CALENDAR);
// Conversations
put("eu.siacs.conversations", NotificationType.CONVERSATIONS);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
/* Copyright (C) 2016-2017 Andreas Shimokawa, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -27,4 +27,6 @@ public class CalendarEventSpec {
public int durationInSeconds;
public String title;
public String description;
public String location;
public boolean allDay;
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
Gobbetti, Daniel Hauck
This file is part of Gadgetbridge.
@ -21,15 +21,21 @@ import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Instances;
import android.text.format.Time;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Objects;
public class CalendarEvents {
private static final Logger LOG = LoggerFactory.getLogger(CalendarEvents.class);
// needed for pebble: time, duration, layout, reminders, actions
// layout: type, title, subtitle, body (max 512), tinyIcon, smallIcon, largeIcon
@ -41,13 +47,15 @@ public class CalendarEvents {
private static final String[] EVENT_INSTANCE_PROJECTION = new String[]{
Instances._ID,
Instances.BEGIN,
Instances.END,
Instances.EVENT_ID,
Instances.DURATION,
Instances.TITLE,
Instances.DESCRIPTION,
Instances.EVENT_LOCATION,
Instances.CALENDAR_DISPLAY_NAME
Instances.CALENDAR_DISPLAY_NAME,
Instances.ALL_DAY
};
private static final int lookahead_days = 7;
@ -62,28 +70,37 @@ public class CalendarEvents {
private boolean fetchSystemEvents(Context mContext) {
Calendar cal = GregorianCalendar.getInstance();
Long dtStart = cal.getTime().getTime();
Long dtStart = cal.getTimeInMillis();
cal.add(Calendar.DATE, lookahead_days);
Long dtEnd = cal.getTime().getTime();
Long dtEnd = cal.getTimeInMillis();
Uri.Builder eventsUriBuilder = CalendarContract.Instances.CONTENT_URI.buildUpon();
Uri.Builder eventsUriBuilder = Instances.CONTENT_URI.buildUpon();
ContentUris.appendId(eventsUriBuilder, dtStart);
ContentUris.appendId(eventsUriBuilder, dtEnd);
Uri eventsUri = eventsUriBuilder.build();
try (Cursor evtCursor = mContext.getContentResolver().query(eventsUri, EVENT_INSTANCE_PROJECTION, null, null, CalendarContract.Instances.BEGIN + " ASC")) {
try (Cursor evtCursor = mContext.getContentResolver().query(eventsUri, EVENT_INSTANCE_PROJECTION, null, null, Instances.BEGIN + " ASC")) {
if (evtCursor == null || evtCursor.getCount() == 0) {
return false;
}
while (evtCursor.moveToNext()) {
long start = evtCursor.getLong(1);
long end = evtCursor.getLong(2);
if (end == 0) {
LOG.info("no end time, will parse duration string");
Time time = new Time(); //FIXME: deprecated FTW
time.parse(evtCursor.getString(3));
end = start + time.toMillis(false);
}
CalendarEvent calEvent = new CalendarEvent(
evtCursor.getLong(1),
evtCursor.getLong(2),
evtCursor.getLong(3),
start,
end,
evtCursor.getLong(0),
evtCursor.getString(4),
evtCursor.getString(5),
evtCursor.getString(6),
evtCursor.getString(7)
evtCursor.getString(7),
!evtCursor.getString(8).equals("0")
);
calendarEventList.add(calEvent);
}
@ -91,7 +108,7 @@ public class CalendarEvents {
}
}
public class CalendarEvent {
public static class CalendarEvent {
private long begin;
private long end;
private long id;
@ -99,8 +116,9 @@ public class CalendarEvents {
private String description;
private String location;
private String calName;
private boolean allDay;
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName) {
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, boolean allDay) {
this.begin = begin;
this.end = end;
this.id = id;
@ -108,6 +126,7 @@ public class CalendarEvents {
this.description = description;
this.location = location;
this.calName = calName;
this.allDay = allDay;
}
public long getBegin() {
@ -155,5 +174,38 @@ public class CalendarEvents {
return calName;
}
public boolean isAllDay() {
return allDay;
}
@Override
public boolean equals(Object other) {
if (other instanceof CalendarEvent) {
CalendarEvent e = (CalendarEvent) other;
return (this.getId() == e.getId()) &&
Objects.equals(this.getTitle(), e.getTitle()) &&
(this.getBegin() == e.getBegin()) &&
Objects.equals(this.getLocation(), e.getLocation()) &&
Objects.equals(this.getDescription(), e.getDescription()) &&
(this.getEnd() == e.getEnd()) &&
Objects.equals(this.getCalName(), e.getCalName()) &&
(this.isAllDay() == e.isAllDay());
} else {
return false;
}
}
@Override
public int hashCode() {
int result = (int) id;
result = 31 * result + Objects.hash(title);
result = 31 * result + Long.valueOf(begin).hashCode();
result = 31 * result + Objects.hash(location);
result = 31 * result + Objects.hash(description);
result = 31 * result + Long.valueOf(end).hashCode();
result = 31 * result + Objects.hash(calName);
result = 31 * result + Boolean.valueOf(allDay).hashCode();
return result;
}
}
}

View File

@ -97,7 +97,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_URI = "uri";
String EXTRA_CONFIG = "config";
String EXTRA_ALARMS = "alarms";
String EXTRA_PERFORM_PAIR = "perform_pair";
String EXTRA_CONNECT_FIRST_TIME = "connect_first_time";
String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps";
String EXTRA_WEATHER_TIMESTAMP = "weather_timestamp";
@ -129,6 +129,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration";
String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title";
String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description";
String EXTRA_CALENDAREVENT_LOCATION = "calendarevent_location";
void start();

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Daniele Gobbetti
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
@ -62,6 +63,16 @@ public class MusicSpec {
this.duration == musicSpec.duration &&
this.trackCount == musicSpec.trackCount &&
this.trackNr == musicSpec.trackNr;
}
@Override
public int hashCode() {
int result = artist != null ? artist.hashCode() : 0;
result = 31 * result + (album != null ? album.hashCode() : 0);
result = 31 * result + (track != null ? track.hashCode() : 0);
result = 31 * result + duration;
result = 31 * result + trackCount;
result = 31 * result + trackNr;
return result;
}
}

View File

@ -29,6 +29,7 @@ public enum NotificationType {
GENERIC_EMAIL(PebbleIconID.GENERIC_EMAIL, PebbleColor.JaegerGreen),
GENERIC_NAVIGATION(PebbleIconID.LOCATION, PebbleColor.Orange),
GENERIC_SMS(PebbleIconID.GENERIC_SMS, PebbleColor.VividViolet),
GENERIC_CALENDAR(PebbleIconID.TIMELINE_CALENDAR, PebbleColor.Blue),
FACEBOOK(PebbleIconID.NOTIFICATION_FACEBOOK, PebbleColor.Liberty),
FACEBOOK_MESSENGER(PebbleIconID.NOTIFICATION_FACEBOOK_MESSENGER, PebbleColor.VeryLightBlue),
RIOT(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.LavenderIndigo),
@ -39,8 +40,8 @@ public enum NotificationType {
GENERIC_ALARM_CLOCK(PebbleIconID.ALARM_CLOCK, PebbleColor.Red);
// Note: if you add any more constants, update all clients as well
public int icon;
public byte color;
public final int icon;
public final byte color;
NotificationType(int icon, byte color) {
this.icon = icon;

View File

@ -88,6 +88,14 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
this.context = context;
}
/**
* Default implementation just calls #connect()
*/
@Override
public boolean connectFirstTime() {
return connect();
}
@Override
public boolean isConnected() {
return gbDevice.isConnected();
@ -310,6 +318,8 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
context.getString(R.string.notif_battery_low_bigtext_number_of_charges, String.valueOf(deviceEvent.numCharges))
: ""
, context);
} else {
GB.removeBatteryNotification(context);
}
gbDevice.sendDeviceUpdateIntent(context);

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Avamander, Carsten Pfeiffer,
Daniele Gobbetti, ivanovlev, Julien Pivotto, Kasha, Sergey Trofimov, Steffen
Liebergeld, Uwe Hermann
Daniele Gobbetti, Daniel Hauck, ivanovlev, João Paulo Barraca, Julien
Pivotto, Kasha, Sergey Trofimov, Steffen Liebergeld, Uwe Hermann
This file is part of Gadgetbridge.
@ -18,6 +18,8 @@
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;
import android.bluetooth.BluetoothDevice;
@ -26,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;
@ -41,12 +45,12 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothPairingRequestReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.MusicPlaybackReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.PebbleReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.PhoneCallReceiver;
@ -107,6 +111,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_BOO
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_DESCRIPTION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_ID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_LOCATION;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TITLE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALENDAREVENT_TYPE;
@ -116,6 +121,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAL
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONNECT_FIRST_TIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST;
@ -137,7 +143,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SUBJECT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TITLE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_PERFORM_PAIR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_URI;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_VIBRATION_INTENSITY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEATHER_CURRENTCONDITION;
@ -153,6 +158,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_WEA
public class DeviceCommunicationService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final Logger LOG = LoggerFactory.getLogger(DeviceCommunicationService.class);
@SuppressLint("StaticFieldLeak") // only used for test cases
private static DeviceSupportFactory DEVICE_SUPPORT_FACTORY = null;
private boolean mStarted = false;
@ -167,9 +173,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private MusicPlaybackReceiver mMusicPlaybackReceiver = null;
private TimeChangeReceiver mTimeChangeReceiver = null;
private BluetoothConnectReceiver mBlueToothConnectReceiver = null;
private BluetoothPairingRequestReceiver mBlueToothPairingRequestReceiver = null;
private AlarmClockReceiver mAlarmClockReceiver = null;
private AlarmReceiver mAlarmReceiver = null;
private CalendarReceiver mCalendarReceiver = null;
private Random mRandom = new Random();
private final String[] mMusicActions = {
@ -201,22 +209,13 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
String action = intent.getAction();
if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
// FIXME: mGBDevice was null here once
if (mGBDevice.equals(device)) {
if (mGBDevice != null && mGBDevice.equals(device)) {
mGBDevice = device;
boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized());
setReceiversEnableState(enableReceivers);
setReceiversEnableState(enableReceivers, mGBDevice.isInitialized(), DeviceHelper.getInstance().getCoordinator(device));
GB.updateNotification(mGBDevice.getName() + " " + mGBDevice.getStateString(), mGBDevice.isInitialized(), context);
if (device.isInitialized()) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
DBHelper.getDevice(device, session); // implicitly creates the device in database if not present, and updates device attributes
} catch (Exception ignore) {
}
}
} else {
LOG.error("Got ACTION_DEVICE_CHANGED from unexpected device: " + mGBDevice);
LOG.error("Got ACTION_DEVICE_CHANGED from unexpected device: " + device);
}
}
}
@ -250,7 +249,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
String action = intent.getAction();
boolean pair = intent.getBooleanExtra(EXTRA_PERFORM_PAIR, false);
boolean firstTime = intent.getBooleanExtra(EXTRA_CONNECT_FIRST_TIME, false);
if (action == null) {
LOG.info("no action");
@ -310,8 +309,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
DeviceSupport deviceSupport = mFactory.createDeviceSupport(gbDevice);
if (deviceSupport != null) {
setDeviceSupport(deviceSupport);
if (pair) {
deviceSupport.pair();
if (firstTime) {
deviceSupport.connectFirstTime();
} else {
deviceSupport.setAutoReconnect(autoReconnect);
deviceSupport.connect();
@ -380,6 +379,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
calendarEventSpec.durationInSeconds = intent.getIntExtra(EXTRA_CALENDAREVENT_DURATION, -1);
calendarEventSpec.title = intent.getStringExtra(EXTRA_CALENDAREVENT_TITLE);
calendarEventSpec.description = intent.getStringExtra(EXTRA_CALENDAREVENT_DESCRIPTION);
calendarEventSpec.location = intent.getStringExtra(EXTRA_CALENDAREVENT_LOCATION);
mDeviceSupport.onAddCalendarEvent(calendarEventSpec);
break;
}
@ -404,7 +404,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
case ACTION_DISCONNECT: {
mDeviceSupport.dispose();
if (mGBDevice != null && mGBDevice.getState() == GBDevice.State.WAITING_FOR_RECONNECT) {
setReceiversEnableState(false);
setReceiversEnableState(false, false, null);
mGBDevice.setState(GBDevice.State.NOT_CONNECTED);
mGBDevice.sendDeviceUpdateIntent(this);
}
@ -582,9 +582,35 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
private void setReceiversEnableState(boolean enable) {
private void setReceiversEnableState(boolean enable, boolean initialized, DeviceCoordinator coordinator) {
LOG.info("Setting broadcast receivers to: " + enable);
if (enable && initialized && coordinator != null && coordinator.supportsCalendarEvents()) {
if (mCalendarReceiver == null && getPrefs().getBoolean("enable_calendar_sync", true)) {
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();
registerReceiver(mAlarmReceiver, new IntentFilter("DAILY_ALARM"));
}
} else {
if (mCalendarReceiver != null) {
unregisterReceiver(mCalendarReceiver);
mCalendarReceiver = null;
}
if (mAlarmReceiver != null) {
unregisterReceiver(mAlarmReceiver);
mAlarmReceiver = null;
}
}
if (enable) {
if (mPhoneCallReceiver == null) {
mPhoneCallReceiver = new PhoneCallReceiver();
@ -620,9 +646,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mBlueToothConnectReceiver = new BluetoothConnectReceiver(this);
registerReceiver(mBlueToothConnectReceiver, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED));
}
if (mAlarmReceiver == null) {
mAlarmReceiver = new AlarmReceiver();
registerReceiver(mAlarmReceiver, new IntentFilter("DAILY_ALARM"));
if (mBlueToothPairingRequestReceiver == null) {
mBlueToothPairingRequestReceiver = new BluetoothPairingRequestReceiver(this);
registerReceiver(mBlueToothPairingRequestReceiver, new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST));
}
if (mAlarmClockReceiver == null) {
mAlarmClockReceiver = new AlarmClockReceiver();
@ -656,9 +682,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mBlueToothConnectReceiver);
mBlueToothConnectReceiver = null;
}
if (mAlarmReceiver != null) {
unregisterReceiver(mAlarmReceiver);
mAlarmReceiver = null;
if (mBlueToothPairingRequestReceiver != null) {
unregisterReceiver(mBlueToothPairingRequestReceiver);
mBlueToothPairingRequestReceiver = null;
}
if (mAlarmClockReceiver != null) {
unregisterReceiver(mAlarmClockReceiver);
@ -677,7 +704,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
setReceiversEnableState(false); // disable BroadcastReceivers
setReceiversEnableState(false, false, null); // disable BroadcastReceivers
setDeviceSupport(null);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

View File

@ -52,6 +52,27 @@ public interface DeviceSupport extends EventHandler {
*/
boolean isConnected();
/**
* Attempts an initial connection to the device, typically after the user "discovered"
* and connects to it for the first time. Some implementations may perform an additional
* initialization or application-level pairing compared to the regular {@link #connect()}.
* <p/>
* Implementations may perform the connection in a synchronous or asynchronous way.
* Returns true if a connection attempt was made. If the implementation is synchronous
* it may also return true if the connection was successfully established, however
* callers shall not rely on that.
* <p/>
* The actual connection state change (successful or not) will be reported via the
* #getDevice device as a device change Intent.
*
* Note: the default implementation {@link AbstractDeviceSupport#connectFirstTime()} just
* calls {@link #connect()}
*
* @see #connect()
* @see GBDevice#ACTION_DEVICE_CHANGED
*/
boolean connectFirstTime();
/**
* Attempts to establish a connection to the device. Implementations may perform
* the connection in a synchronous or asynchronous way.
@ -62,6 +83,7 @@ public interface DeviceSupport extends EventHandler {
* The actual connection state change (successful or not) will be reported via the
* #getDevice device as a device change Intent.
*
* @see #connectFirstTime()
* @see GBDevice#ACTION_DEVICE_CHANGED
*/
boolean connect();
@ -92,14 +114,6 @@ public interface DeviceSupport extends EventHandler {
*/
boolean getAutoReconnect();
/**
* Attempts to pair and connect this device with the gadget device. Success
* will be reported via a device change Intent.
*
* @see GBDevice#ACTION_DEVICE_CHANGED
*/
void pair();
/**
* Returns the associated device this instance communicates with.
*/

View File

@ -0,0 +1,95 @@
/* 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.service;
import android.app.ActivityManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.Process;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
/**
* Original source by xinghui - see https://gist.github.com/xinghui/b2ddd8cffe55c4b62f5d8846d5545bf9
* NB: no license specified in the source code as of 2017-04-19
*/
public class NotificationCollectorMonitorService extends Service {
private static final Logger LOG = LoggerFactory.getLogger(NotificationCollectorMonitorService.class);
@Override
public void onCreate() {
super.onCreate();
ensureCollectorRunning();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
private void ensureCollectorRunning() {
ComponentName collectorComponent = new ComponentName(this, NotificationListener.class);
LOG.info("ensureCollectorRunning collectorComponent: " + collectorComponent);
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
boolean collectorRunning = false;
List<ActivityManager.RunningServiceInfo> runningServices = manager.getRunningServices(Integer.MAX_VALUE);
if (runningServices == null) {
LOG.info("ensureCollectorRunning() runningServices is NULL");
return;
}
for (ActivityManager.RunningServiceInfo service : runningServices) {
if (service.service.equals(collectorComponent)) {
LOG.warn("ensureCollectorRunning service - pid: " + service.pid + ", currentPID: " + Process.myPid() + ", clientPackage: " + service.clientPackage + ", clientCount: " + service.clientCount
+ ", clientLabel: " + ((service.clientLabel == 0) ? "0" : "(" + getResources().getString(service.clientLabel) + ")"));
if (service.pid == Process.myPid() /*&& service.clientCount > 0 && !TextUtils.isEmpty(service.clientPackage)*/) {
collectorRunning = true;
}
}
}
if (collectorRunning) {
LOG.debug("ensureCollectorRunning: collector is running");
return;
}
LOG.debug("ensureCollectorRunning: collector not running, reviving...");
toggleNotificationListenerService();
}
private void toggleNotificationListenerService() {
LOG.debug("toggleNotificationListenerService() called");
ComponentName thisComponent = new ComponentName(this, NotificationListener.class);
PackageManager pm = getPackageManager();
pm.setComponentEnabledSetting(thisComponent, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(thisComponent, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

View File

@ -72,6 +72,11 @@ public class ServiceDeviceSupport implements DeviceSupport {
return delegate.isConnected();
}
@Override
public boolean connectFirstTime() {
return delegate.connectFirstTime();
}
@Override
public boolean connect() {
return delegate.connect();
@ -112,11 +117,6 @@ public class ServiceDeviceSupport implements DeviceSupport {
return delegate.useAutoConnect();
}
@Override
public void pair() {
delegate.pair();
}
private boolean checkBusy(String notificationKind) {
if (!flags.contains(Flags.BUSY_CHECKING)) {
return false;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, JohnnySun
/* Copyright (C) 2016-2017 Carsten Pfeiffer, João Paulo Barraca, JohnnySun
This file is part of Gadgetbridge.
@ -101,6 +101,7 @@ public class BleNamesResolver {
mServices.put("00001804-0000-1000-8000-00805f9b34fb", "Tx Power");
mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi Service)");
mServices.put("00001530-0000-3512-2118-0009af100700", "(Propr: Xiaomi Weight Service)");
mServices.put("14701820-620a-3973-7c78-9cfff0876abd", "(Propr: HPLUS Service)");
mCharacteristics.put("00002a43-0000-1000-8000-00805f9b34fb", "Alert AlertCategory ID");
@ -185,6 +186,8 @@ public class BleNamesResolver {
mCharacteristics.put("00002a07-0000-1000-8000-00805f9b34fb", "Tx Power Level");
mCharacteristics.put("00002a45-0000-1000-8000-00805f9b34fb", "Unread Alert Status");
mCharacteristics.put("14702856-620a-3973-7c78-9cfff0876abd", "(Propr: HPLUS Control)");
mCharacteristics.put("14702853-620a-3973-7c78-9cfff0876abd", "(Propr: HPLUS Measurements)");
mValueFormats.put(Integer.valueOf(52), "32bit float");
mValueFormats.put(Integer.valueOf(50), "16bit float");
mValueFormats.put(Integer.valueOf(34), "16bit signed int");

View File

@ -225,7 +225,7 @@ public final class BtLEQueue {
// alive (we do not close() it). Unfortunately we sometimes have problems
// reconnecting automatically, so we try to fix this by re-creating mBluetoothGatt.
// Not sure if this actually works without re-initializing the device...
if (status != 0) {
if (mBluetoothGatt != null) {
if (!wasInitialized || !maybeReconnect()) {
disconnect(); // ensure that we start over cleanly next time
}

View File

@ -30,7 +30,7 @@ public class ValueDecoder {
int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
if (percent > 100 || percent < 0) {
LOG.warn("Unexpected percent value: " + percent + ": " + GattCharacteristic.toString(characteristic));
percent = Math.max(100, Math.min(0, percent));
percent = Math.min(100, Math.max(0, percent));
}
return percent;
}

View File

@ -51,7 +51,13 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
*/
public int heartRate;
public HPlusDataRecordDaySlot(byte[] data) {
private int age = 0;
/**
* Raw intensity calculated from calories
*/
public int intensity;
public HPlusDataRecordDaySlot(byte[] data, int age) {
super(data, TYPE_DAY_SLOT);
int a = (data[4] & 0xFF) * 256 + (data[5] & 0xFF);
@ -77,6 +83,8 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
slotTime.set(Calendar.SECOND, 0);
timestamp = (int) (slotTime.getTimeInMillis() / 1000L);
this.age = age;
}
public String toString(){
@ -101,5 +109,12 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
}
secondsInactive += other.secondsInactive;
intensity = (int) ((100*heartRate)/(208-0.7*age));
}
public boolean isValid(){
return steps != 0 || secondsInactive != 0 || heartRate != -1;
}
}

View File

@ -66,7 +66,7 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
*/
public int intensity;
public HPlusDataRecordRealtime(byte[] data) {
public HPlusDataRecordRealtime(byte[] data, int age) {
super(data, TYPE_REALTIME);
if (data.length < 15) {
@ -91,7 +91,7 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
heartRate = ActivitySample.NOT_MEASURED;
}
else {
intensity = (int) (100 * Math.max(0, Math.min((heartRate - 60) / 120.0, 1))); // TODO: Calculate a proper value
intensity = (int) ((100*heartRate)/(208-0.7*age));
activityKind = ActivityKind.TYPE_UNKNOWN;
}
}

View File

@ -24,6 +24,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -65,6 +66,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
private int DAY_SUMMARY_SYNC_PERIOD = 24 * 60 * 60;
private int DAY_SUMMARY_SYNC_RETRY_PERIOD = 30;
private int HELLO_PERIOD = 60 * 2;
private boolean mQuit = false;
private HPlusSupport mHPlusSupport;
@ -76,6 +79,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
private Calendar mGetSleepTime = GregorianCalendar.getInstance();
private Calendar mGetDaySummaryTime = GregorianCalendar.getInstance();
private Calendar mHelloTime = GregorianCalendar.getInstance();
private boolean mSlotsInitialSync = true;
private HPlusDataRecordRealtime prevRealTimeRecord = null;
@ -88,7 +93,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public HPlusHandlerThread(GBDevice gbDevice, Context context, HPlusSupport hplusSupport) {
super(gbDevice, context);
LOG.info("Initializing HPlus Handler Thread");
mQuit = false;
mHPlusSupport = hplusSupport;
@ -118,9 +123,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
break;
}
if(!mHPlusSupport.getDevice().isConnected()){
if(gbDevice.getState() == GBDevice.State.NOT_CONNECTED){
quit();
break;
}
Calendar now = GregorianCalendar.getInstance();
@ -137,25 +141,35 @@ class HPlusHandlerThread extends GBDeviceIoThread {
requestDaySummaryData();
}
if(now.compareTo(mHelloTime) > 0){
sendHello();
}
now = GregorianCalendar.getInstance();
waitTime = Math.min(mGetDaySummaryTime.getTimeInMillis(), Math.min(mGetDaySlotsTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis())) - now.getTimeInMillis();
waitTime = Math.min(mGetDaySummaryTime.getTimeInMillis(), Math.min(mGetDaySlotsTime.getTimeInMillis(), Math.min(mHelloTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis()))) - now.getTimeInMillis();
}
}
@Override
public void quit() {
LOG.info("HPlus: Quit Handler Thread");
mQuit = true;
synchronized (waitObject) {
waitObject.notify();
}
}
public void sync() {
LOG.info("HPlus: Starting data synchronization");
mGetSleepTime.setTimeInMillis(0);
mGetDaySlotsTime.setTimeInMillis(0);
mGetDaySummaryTime.setTimeInMillis(0);
mLastSleepDayReceived.setTimeInMillis(0);
mHelloTime = GregorianCalendar.getInstance();
mHelloTime.add(Calendar.SECOND, HELLO_PERIOD);
mSlotsInitialSync = true;
mLastSlotReceived = -1;
@ -163,19 +177,42 @@ class HPlusHandlerThread extends GBDeviceIoThread {
mCurrentDaySlot = null;
mDaySlotRecords.clear();
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
try {
if(!mHPlusSupport.isConnected())
mHPlusSupport.connect();
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
builder.queue(mHPlusSupport.getQueue());
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION});
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
LOG.warn("HPlus: Synchronization exception: " + e);
}
synchronized (waitObject) {
waitObject.notify();
}
}
public void sendHello(){
try {
TransactionBuilder builder = new TransactionBuilder("hello");
builder.write(mHPlusSupport.ctrlCharacteristic, HPlusConstants.CMD_ACTION_HELLO);
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
}
mHelloTime = GregorianCalendar.getInstance();
mHelloTime.add(Calendar.SECOND, HELLO_PERIOD);
synchronized (waitObject) {
waitObject.notify();
}
}
/**
* Process a message containing information regarding a day slot
* A slot summarizes 10 minutes of data
@ -183,14 +220,14 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingDaySlotData(byte[] data) {
public boolean processIncomingDaySlotData(byte[] data, int age) {
HPlusDataRecordDaySlot record;
try{
record = new HPlusDataRecordDaySlot(data);
record = new HPlusDataRecordDaySlot(data, age);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
LOG.info((e.getMessage()));
return false;
}
@ -254,13 +291,18 @@ class HPlusHandlerThread extends GBDeviceIoThread {
List<HPlusHealthActivitySample> samples = new ArrayList<>();
for (HPlusDataRecordDaySlot storedRecord : mDaySlotRecords) {
//Invalid records (no data) will be ignored
if(!storedRecord.isValid())
continue;
HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp);
sample.setRawHPlusHealthData(storedRecord.getRawData());
sample.setSteps(storedRecord.steps);
sample.setHeartRate(storedRecord.heartRate);
sample.setRawKind(storedRecord.type);
sample.setRawIntensity(record.intensity);
sample.setProvider(provider);
samples.add(sample);
}
@ -269,9 +311,9 @@ class HPlusHandlerThread extends GBDeviceIoThread {
mDaySlotRecords.clear();
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
LOG.info((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
LOG.info(ex.getMessage());
}
}
@ -293,7 +335,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
try{
record = new HPlusDataRecordSleep(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
LOG.info((e.getMessage()));
return false;
}
@ -326,7 +368,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
provider.addGBActivitySample(sample);
} catch (Exception ex) {
LOG.debug(ex.getMessage());
LOG.info(ex.getMessage());
}
mGetSleepTime = GregorianCalendar.getInstance();
@ -341,13 +383,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processRealtimeStats(byte[] data) {
public boolean processRealtimeStats(byte[] data, int age) {
HPlusDataRecordRealtime record;
try{
record = new HPlusDataRecordRealtime(data);
record = new HPlusDataRecordRealtime(data, age);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
LOG.info((e.getMessage()));
return false;
}
@ -397,9 +439,9 @@ class HPlusHandlerThread extends GBDeviceIoThread {
//TODO: Handle Active Time. With Overlay?
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
LOG.info((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
LOG.info(ex.getMessage());
}
return true;
}
@ -417,7 +459,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
try{
record = new HPlusDataRecordDaySummary(data);
} catch(IllegalArgumentException e){
LOG.debug((e.getMessage()));
LOG.info((e.getMessage()));
return false;
}
@ -437,9 +479,9 @@ class HPlusHandlerThread extends GBDeviceIoThread {
sample.setProvider(provider);
provider.addGBActivitySample(sample);
} catch (GBException ex) {
LOG.debug((ex.getMessage()));
LOG.info((ex.getMessage()));
} catch (Exception ex) {
LOG.debug(ex.getMessage());
LOG.info(ex.getMessage());
}
mGetDaySummaryTime = GregorianCalendar.getInstance();
@ -454,11 +496,23 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @return boolean indicating success or fail
*/
public boolean processVersion(byte[] data) {
int major = data[2] & 0xFF;
int minor = data[1] & 0xFF;
int major, minor;
if(data.length >= 11){
major = data[10] & 0xFF;
minor = data[9] & 0xFF;
int hwMajor = data[2] & 0xFF;
int hwMinor = data[1] & 0xFF;
getDevice().setFirmwareVersion2(hwMajor + "." + hwMinor);
mHPlusSupport.setUnicodeSupport((data[3] != 0));
}else {
major = data[2] & 0xFF;
minor = data[1] & 0xFF;
}
getDevice().setFirmwareVersion(major + "." + minor);
getDevice().sendDeviceUpdateIntent(getContext());
return true;
@ -468,10 +522,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* Issue a message requesting the next batch of sleep data
*/
private void requestNextSleepData() {
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
builder.queue(mHPlusSupport.getQueue());
try {
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
}
mGetSleepTime = GregorianCalendar.getInstance();
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_RETRY_PERIOD);
@ -519,19 +576,26 @@ class HPlusHandlerThread extends GBDeviceIoThread {
mLastSlotRequested = nextHour * 6 + (nextMinute / 10);
byte[] msg = new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY, hour, minute, nextHour, nextMinute};
try {
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
builder.queue(mHPlusSupport.getQueue());
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
}
}
/**
* Request a batch of data with the summary of the previous days
*/
public void requestDaySummaryData(){
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
builder.queue(mHPlusSupport.getQueue());
try {
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
}
mGetDaySummaryTime = GregorianCalendar.getInstance();
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_RETRY_PERIOD);
}
@ -560,4 +624,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
return sample;
}
public void setHPlusSupport(HPlusSupport HPlusSupport) {
LOG.info("Updating HPlusSupport object");
this.mHPlusSupport = HPlusSupport;
}
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, ivanovlev, João
Paulo Barraca
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
ivanovlev, João Paulo Barraca
This file is part of Gadgetbridge.
@ -38,12 +38,14 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -52,14 +54,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -72,6 +73,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
public BluetoothGattCharacteristic ctrlCharacteristic = null;
public BluetoothGattCharacteristic measureCharacteristic = null;
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
private HPlusHandlerThread syncHelper;
private DeviceType deviceType = DeviceType.UNKNOWN;
@ -87,11 +90,9 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
public HPlusSupport(DeviceType type) {
super(LOG);
LOG.info("HPlusSupport Instance Created");
deviceType = type;
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(HPlusConstants.UUID_SERVICE_HP);
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
@ -102,7 +103,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void dispose() {
LOG.debug("Dispose");
LOG.info("Dispose");
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
broadcastManager.unregisterReceiver(mReceiver);
@ -113,31 +114,36 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
LOG.debug("Initializing");
LOG.info("Initializing");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
gbDevice.setState(GBDevice.State.INITIALIZING);
gbDevice.sendDeviceUpdateIntent(getContext());
measureCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE);
ctrlCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_CONTROL);
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
syncHelper = new HPlusHandlerThread(getDevice(), getContext(), this);
//Initialize device
sendUserInfo(builder); //Sync preferences
requestDeviceInfo(builder);
setInitialized(builder);
syncHelper.start();
builder.notify(getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE), true);
builder.setGattCallback(this);
builder.notify(measureCharacteristic, true);
//Initialize device
sendUserInfo(builder); //Sync preferences
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
if(syncHelper == null) {
syncHelper = new HPlusHandlerThread(getDevice(), getContext(), this);
syncHelper.start();
}
syncHelper.sync();
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
requestDeviceInfo(builder);
LOG.info("Initialization Done");
return builder;
}
@ -154,7 +160,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
private HPlusSupport syncPreferences(TransactionBuilder transaction) {
if(deviceType == DeviceType.HPLUS) {
if (deviceType == DeviceType.HPLUS) {
setSIT(transaction); //Sync SIT Interval
}
@ -252,7 +258,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
//Makibes F68 doesn't like this command.
//Just ignore.
if(deviceType == DeviceType.MAKIBESF68){
if (deviceType == DeviceType.MAKIBESF68) {
return this;
}
@ -286,7 +292,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
}
private HPlusSupport setWeight(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserWeight(getDevice().getAddress());
byte value = HPlusCoordinator.getUserWeight();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_WEIGHT,
value
@ -296,7 +302,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
}
private HPlusSupport setHeight(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserHeight(getDevice().getAddress());
byte value = HPlusCoordinator.getUserHeight();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_HEIGHT,
value
@ -307,7 +313,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
private HPlusSupport setAge(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserAge(getDevice().getAddress());
byte value = HPlusCoordinator.getUserAge();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_AGE,
value
@ -317,7 +323,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
}
private HPlusSupport setGender(TransactionBuilder transaction) {
byte value = HPlusCoordinator.getUserGender(getDevice().getAddress());
byte value = HPlusCoordinator.getUserGender();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_GENDER,
value
@ -328,7 +334,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
private HPlusSupport setGoal(TransactionBuilder transaction) {
int value = HPlusCoordinator.getGoal(getDevice().getAddress());
int value = HPlusCoordinator.getGoal();
transaction.write(ctrlCharacteristic, new byte[]{
HPlusConstants.CMD_SET_GOAL,
(byte) ((value / 256) & 0xff),
@ -368,7 +374,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
byte hour = HPlusConstants.ARG_ALARM_DISABLE;
byte minute = HPlusConstants.ARG_ALARM_DISABLE;
if(t != null){
if (t != null) {
hour = (byte) t.get(Calendar.HOUR_OF_DAY);
minute = (byte) t.get(Calendar.MINUTE);
}
@ -403,22 +409,11 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
return this;
}
private void setInitialized(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
public void pair() {
LOG.debug("Pair");
}
private void handleDeviceInfo(DeviceInfo info) {
LOG.warn("Device info: " + info);
}
@ -426,7 +421,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void onNotification(NotificationSpec notificationSpec) {
//TODO: Show different notifications according to source as Band supports this
//LOG.debug("OnNotification: Title: "+notificationSpec.title+" Body: "+notificationSpec.body+" Source: "+notificationSpec.sourceName+" Sender: "+notificationSpec.sender+" Subject: "+notificationSpec.subject);
//LOG.info("OnNotification: Title: "+notificationSpec.title+" Body: "+notificationSpec.body+" Source: "+notificationSpec.sourceName+" Sender: "+notificationSpec.sender+" Subject: "+notificationSpec.subject);
showText(notificationSpec.title, notificationSpec.body);
}
@ -437,40 +432,48 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void onSetTime() {
TransactionBuilder builder = new TransactionBuilder("time");
setCurrentDate(builder);
setCurrentTime(builder);
try {
TransactionBuilder builder = performInitialized("time");
builder.queue(getQueue());
setCurrentDate(builder);
setCurrentTime(builder);
performConnected(builder.getTransaction());
}catch(IOException e){
}
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
TransactionBuilder builder = new TransactionBuilder("alarm");
for (Alarm alarm : alarms) {
try {
TransactionBuilder builder = performInitialized("alarm");
if (!alarm.isEnabled())
continue;
for (Alarm alarm : alarms) {
if (alarm.isSmartWakeup()) //Not available
continue;
if (!alarm.isEnabled())
continue;
Calendar t = alarm.getAlarmCal();
setAlarm(builder, t);
builder.queue(getQueue());
if (alarm.isSmartWakeup()) //Not available
continue;
GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO);
Calendar t = alarm.getAlarmCal();
setAlarm(builder, t);
builder.queue(getQueue());
return; //Only first alarm
}
GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO);
setAlarm(builder, null);
builder.queue(getQueue());
return; //Only first alarm
}
setAlarm(builder, null);
performConnected(builder.getTransaction());
GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO);
}catch(Exception e){}
GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO);
}
@ -487,12 +490,11 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
LOG.debug("Canned Messages: " + cannedMessagesSpec);
LOG.info("Canned Messages: " + cannedMessagesSpec);
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
}
@Override
@ -537,45 +539,57 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void onFetchActivityData() {
if (syncHelper != null)
syncHelper.sync();
if (syncHelper == null){
syncHelper = new HPlusHandlerThread(gbDevice, getContext(), this);
syncHelper.start();
}
syncHelper.sync();
}
@Override
public void onReboot() {
getQueue().clear();
try {
getQueue().clear();
TransactionBuilder builder = new TransactionBuilder("Shutdown");
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SHUTDOWN, HPlusConstants.ARG_SHUTDOWN_EN});
builder.queue(getQueue());
TransactionBuilder builder = performInitialized("Shutdown");
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SHUTDOWN, HPlusConstants.ARG_SHUTDOWN_EN});
performConnected(builder.getTransaction());
}catch(Exception e){
}
}
@Override
public void onHeartRateTest() {
getQueue().clear();
try{
TransactionBuilder builder = performInitialized("HeartRateTest");
TransactionBuilder builder = new TransactionBuilder("HeartRateTest");
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_HEARTRATE_STATE, HPlusConstants.ARG_HEARTRATE_MEASURE_ON}); //Set Real Time... ?
performConnected(builder.getTransaction());
}catch(Exception e){
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_HEARTRATE_STATE, HPlusConstants.ARG_HEARTRATE_MEASURE_ON}); //Set Real Time... ?
builder.queue(getQueue());
}
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
getQueue().clear();
try {
TransactionBuilder builder = performInitialized("realTimeHeartMeasurement");
byte state;
TransactionBuilder builder = new TransactionBuilder("realTimeHeartMeasurement");
byte state;
if (enable)
state = HPlusConstants.ARG_HEARTRATE_ALLDAY_ON;
else
state = HPlusConstants.ARG_HEARTRATE_ALLDAY_OFF;
if (enable)
state = HPlusConstants.ARG_HEARTRATE_ALLDAY_ON;
else
state = HPlusConstants.ARG_HEARTRATE_ALLDAY_OFF;
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALLDAY_HRM, state});
builder.queue(getQueue());
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALLDAY_HRM, state});
performConnected(builder.getTransaction());
}catch(Exception e){
}
}
@Override
@ -584,7 +598,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
TransactionBuilder builder = performInitialized("findMe");
setFindMe(builder, start);
builder.queue(getQueue());
performConnected(builder.getTransaction());
} catch (IOException e) {
GB.toast(getContext(), "Error toggling Find Me: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
@ -593,8 +607,6 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void onSetConstantVibration(int intensity) {
getQueue().clear();
try {
TransactionBuilder builder = performInitialized("vibration");
@ -605,7 +617,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
msg[i + 1] = (byte) "GadgetBridge".charAt(i);
builder.write(ctrlCharacteristic, msg);
builder.queue(getQueue());
performConnected(builder.getTransaction());
} catch (IOException e) {
GB.toast(getContext(), "Error setting Vibration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
@ -634,13 +646,13 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
@Override
public void onSendConfiguration(String config) {
LOG.debug("Send Configuration: " + config);
LOG.info("Send Configuration: " + config);
}
@Override
public void onTestNewFunction() {
LOG.debug("Test New Function");
LOG.info("Test New Function");
}
@Override
@ -648,17 +660,13 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
}
public void setUnicodeSupport(boolean support){
HPlusCoordinator.setUnicodeSupport(gbDevice.getAddress(), support);
}
private void showIncomingCall(String name, String rawNumber) {
try {
StringBuilder number = new StringBuilder();
//Clean up number as the device only accepts digits
for(char c : rawNumber.toCharArray()){
if(Character.isDigit(c)){
number.append(c);
}
}
TransactionBuilder builder = performInitialized("incomingCall");
@ -668,39 +676,50 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
//Show Call Icon
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_CALL, HPlusConstants.ARG_INCOMING_CALL});
byte[] msg = new byte[13];
if(name != null) {
byte[] msg = new byte[13];
//Show call name
for (int i = 0; i < msg.length; i++)
msg[i] = ' ';
//Show call name
byte[] nameBytes = encodeStringToDevice(name);
for (int i = 0; i < nameBytes.length && i < (msg.length - 1); i++)
msg[i + 1] = nameBytes[i];
for (int i = 0; i < msg.length; i++)
msg[i] = ' ';
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME;
builder.write(ctrlCharacteristic, msg);
byte[] nameBytes = encodeStringToDevice(name);
for (int i = 0; i < nameBytes.length && i < (msg.length - 1); i++)
msg[i + 1] = nameBytes[i];
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME_CN;
builder.write(ctrlCharacteristic, msg);
}
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME;
builder.write(ctrlCharacteristic, msg);
if(rawNumber != null) {
StringBuilder number = new StringBuilder();
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME_CN;
builder.write(ctrlCharacteristic, msg);
//Clean up number as the device only accepts digits
for (char c : rawNumber.toCharArray()) {
if (Character.isDigit(c)) {
number.append(c);
}
}
builder.wait(200);
msg = msg.clone();
byte[] msg = new byte[13];
//Show call number
for (int i = 0; i < msg.length; i++)
msg[i] = ' ';
//Show call number
for (int i = 0; i < msg.length; i++)
msg[i] = ' ';
for (int i = 0; i < number.length() && i < (msg.length - 1); i++)
msg[i + 1] = (byte) number.charAt(i);
for (int i = 0; i < number.length() && i < (msg.length - 1); i++)
msg[i + 1] = (byte) number.charAt(i);
msg[0] = HPlusConstants.CMD_SET_INCOMING_CALL_NUMBER;
msg[0] = HPlusConstants.CMD_SET_INCOMING_CALL_NUMBER;
builder.write(ctrlCharacteristic, msg);
builder.wait(200);
builder.write(ctrlCharacteristic, msg);
}
builder.queue(getQueue());
performConnected(builder.getTransaction());
} catch (IOException e) {
GB.toast(getContext(), "Error showing incoming call: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
@ -708,7 +727,6 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
}
private void showText(String title, String body) {
LOG.debug("Show Notification: "+title+" --> "+body);
try {
TransactionBuilder builder = performInitialized("notification");
@ -718,7 +736,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
message = StringUtils.pad(StringUtils.truncate(title, 16), 16); //Limit title to top row
}
if(body != null) {
if (body != null) {
message += body;
}
@ -726,6 +744,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
int length = messageBytes.length / 17;
length = length > 5 ? 5 : length;
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_MESSAGE, HPlusConstants.ARG_INCOMING_MESSAGE});
int remaining = Math.min(255, (messageBytes.length % 17 > 0) ? length + 1 : length);
@ -762,7 +782,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
msg[2] = (byte) remaining;
builder.write(ctrlCharacteristic, msg);
builder.queue(getQueue());
performConnected(builder.getTransaction());
} catch (IOException e) {
GB.toast(getContext(), "Error showing device Notification: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
@ -772,6 +792,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
private void close() {
if (syncHelper != null) {
syncHelper.quit();
syncHelper.interrupt();
syncHelper = null;
}
}
@ -784,26 +805,29 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
* @param s The String to transliterate
* @return An array of bytes ready to be sent to the device
*/
private byte[] encodeStringToDevice(String s){
private byte[] encodeStringToDevice(String s) {
List<Byte> outBytes = new ArrayList<Byte>();
for(int i = 0; i < s.length(); i++){
Character c = s.charAt(i);
byte[] cs;
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);
byte[] cs;
if(HPlusConstants.transliterateMap.containsKey(c)){
cs = new byte[] {HPlusConstants.transliterateMap.get(c)};
}else {
try {
if (HPlusConstants.transliterateMap.containsKey(c)) {
cs = HPlusConstants.transliterateMap.get(c);
} else {
try {
if(HPlusCoordinator.getUnicodeSupport(this.gbDevice.getAddress()))
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();
}
} catch (UnsupportedEncodingException e) {
//Fallback. Result string may be strange, but better than nothing
cs = c.toString().getBytes();
}
for(int j = 0; j < cs.length; j++)
outBytes.add(cs[j]);
}
for (int j = 0; j < cs.length; j++)
outBytes.add(cs[j]);
}
return ArrayUtils.toPrimitive(outBytes.toArray(new Byte[outBytes.size()]));
@ -823,10 +847,15 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
switch (data[0]) {
case HPlusConstants.DATA_VERSION:
case HPlusConstants.DATA_VERSION1:
return syncHelper.processVersion(data);
case HPlusConstants.DATA_STATS:
return syncHelper.processRealtimeStats(data);
boolean result = syncHelper.processRealtimeStats(data, HPlusCoordinator.getUserAge());
if (result) {
processExtraInfo (data);
}
return result;
case HPlusConstants.DATA_SLEEP:
return syncHelper.processIncomingSleepData(data);
@ -836,12 +865,54 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
case HPlusConstants.DATA_DAY_SUMMARY:
case HPlusConstants.DATA_DAY_SUMMARY_ALT:
return syncHelper.processIncomingDaySlotData(data);
return syncHelper.processIncomingDaySlotData(data, HPlusCoordinator.getUserAge());
case HPlusConstants.DATA_UNKNOWN:
return true;
default:
LOG.debug("Unhandled characteristic changed: " + characteristicUUID);
LOG.info("Unhandled characteristic change: " + characteristicUUID + " code: " + Arrays.toString(data));
return true;
}
}
private void processExtraInfo (byte[] data) {
try {
HPlusDataRecordRealtime record = new HPlusDataRecordRealtime(data, HPlusCoordinator.getUserAge());
handleBatteryInfo(record.battery);
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 info = "";
if (record.steps > 0) {
info += DEVINFO_STEP + String.valueOf(record.steps) + " ";
}
if (record.distance > 0) {
info += DEVINFO_DISTANCE + String.valueOf(record.distance) + " ";
}
if (record.calories > 0) {
info += DEVINFO_CALORY + String.valueOf(record.calories) + " ";
}
if (record.heartRate > 0) {
info += DEVINFO_HEART + String.valueOf(record.heartRate) + " ";
}
if (!info.equals("")) {
getDevice().addDeviceInfo(new GenericItem("", info));
}
} catch (IllegalArgumentException e) {
LOG.info((e.getMessage()));
}
}
private void handleBatteryInfo(byte data) {
if (batteryCmd.level != (short) data) {
batteryCmd.level = (short) data;
handleGBDeviceEvent(batteryCmd);
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Daniele Gobbetti
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -197,8 +197,6 @@ public class LiveviewIoThread extends GBDeviceIoThread {
break;
case HEADER_LEN:
int headerSize = 0xff & incoming[0];
if (headerSize < 0)
throw new IOException();
state = ReaderState.HEADER;
incoming = new byte[headerSize];
break;

View File

@ -217,12 +217,13 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
@Override
public void pair() {
public boolean connectFirstTime() {
for (int i = 0; i < 5; i++) {
if (connect()) {
return;
return true;
}
}
return false;
}
public DeviceInfo getDeviceInfo() {

View File

@ -267,13 +267,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
}
@Override
public void pair() {
public boolean connectFirstTime() {
needsAuth = true;
for (int i = 0; i < 5; i++) {
if (connect()) {
return;
}
}
return super.connect();
}
private MiBand2Support sendDefaultNotification(TransactionBuilder builder, SimpleNotification simpleNotification, short repeat, BtLEAction extraAction) {
@ -810,7 +806,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
}
private void handleButtonPressed(byte[] value) {
LOG.info("Button pressed: " + value);
LOG.info("Button pressed");
logMessageContent(value);
}

View File

@ -216,7 +216,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
int len = value.length;
if (len % 4 != 1) {
throw new AssertionError("Unexpected activity array size: " + value);
throw new AssertionError("Unexpected activity array size: " + len);
}
for (int i = 1; i < len; i+=4) {

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Daniele Gobbetti
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
@ -69,8 +70,8 @@ class AppMessageHandlerZalewszczak extends AppMessageHandler {
}
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(KEY_TEMP, (Object) (Math.round(weatherSpec.currentTemp - 273) + "C")));
pairs.add(new Pair<>(KEY_ICON, (Object) (getIconForConditionCode(weatherSpec.currentConditionCode))));
pairs.add(new Pair<Integer, Object>(KEY_TEMP, weatherSpec.currentTemp - 273 + "C"));
pairs.add(new Pair<Integer, Object>(KEY_ICON, getIconForConditionCode(weatherSpec.currentConditionCode)));
byte[] weatherMessage = mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs);
ByteBuffer buf = ByteBuffer.allocate(weatherMessage.length);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Daniele Gobbetti
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -31,15 +31,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
class DatalogSessionAnalytics extends DatalogSession {
private static final Logger LOG = LoggerFactory.getLogger(DatalogSessionAnalytics.class);
private GBDeviceEventBatteryInfo mGBDeviceEventBatteryInfo = new GBDeviceEventBatteryInfo();
private GBDevice mGBDevice;
DatalogSessionAnalytics(byte id, UUID uuid, int timestamp, int tag, byte itemType, short itemSize, GBDevice device) {
super(id, uuid, timestamp, tag, itemType, itemSize);
if (mGBDevice == null || !device.equals(mGBDevice)) { //prevent showing information of other pebble watches when switching devices
mGBDevice = device;
mGBDeviceEventBatteryInfo.state = BatteryState.UNKNOWN;
}
// The default notification should not be too bad (one per hour) but we can override this if needed
//mGBDevice.setBatteryThresholdPercent((short) 5);
@ -56,11 +50,13 @@ class DatalogSessionAnalytics extends DatalogSession {
datalogMessage.position(datalogMessage.position() + 12);
short reportedMilliVolts = datalogMessage.getShort();
LOG.info("Battery reading for TS " + messageTS + " is: " + reportedMilliVolts + " milliVolts, mapped to percentage: " + milliVoltstoPercentage(reportedMilliVolts));
datalogMessage.position(datalogMessage.position() + 2);
byte reportedPercentage = datalogMessage.get();
LOG.info("Battery reading for TS " + messageTS + " is: " + reportedMilliVolts + " milliVolts, percentage: " + reportedPercentage);
if (messageTS > 0 && reportedMilliVolts < 5000) { //some safety checks
mGBDeviceEventBatteryInfo.state = BatteryState.BATTERY_NORMAL;
mGBDeviceEventBatteryInfo.level = milliVoltstoPercentage(reportedMilliVolts);
mGBDeviceEventBatteryInfo.level = reportedPercentage;
return new GBDeviceEvent[]{mGBDeviceEventBatteryInfo, null};
} else { //invalid data, but we ack nevertheless
@ -68,30 +64,4 @@ class DatalogSessionAnalytics extends DatalogSession {
}
}
private short milliVoltstoPercentage(short batteryMilliVolts) {
if (batteryMilliVolts > 4145) { //(4146 is still 100, next reported value is already 90)
return 100;
} else if (batteryMilliVolts > 4053) { //(4054 is still 90, next reported value is already 80)
return 90;
} else if (batteryMilliVolts > 4000) { //guessed
return 80;
} else if (batteryMilliVolts > 3880) { //confirmed
return 70;
} else if (batteryMilliVolts > 3855) { //probably
return 60;
} else if (batteryMilliVolts > 3780) { //3781 is still 50, next reading is 3776 but percentage on pebble unknown
return 50;
} else if (batteryMilliVolts >= 3750) { //3750 is still 40, next reported value is 3746 and already 30
return 40;
} else if (batteryMilliVolts > 3720) { //3723 is still 30, next reported value is 3719 and already 20
return 30;
} else if (batteryMilliVolts > 3680) { //3683 is still 20, next reported value is 3675 and already 10
return 20;
} else if (batteryMilliVolts > 3650) { //3657 is still 10
return 10;
} else {
return 0; //or -1 for invalid?
}
}
}

View File

@ -30,6 +30,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.SimpleTimeZone;
@ -514,6 +515,8 @@ public class PebbleProtocol extends GBDeviceProtocol {
public byte[] encodeAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
long id = calendarEventSpec.id != -1 ? calendarEventSpec.id : mRandom.nextLong();
int iconId;
ArrayList<Pair<Integer, Object>> attributes = new ArrayList<>();
attributes.add(new Pair<>(1, (Object) calendarEventSpec.title));
switch (calendarEventSpec.type) {
case CalendarEventSpec.TYPE_SUNRISE:
iconId = PebbleIconID.SUNRISE;
@ -523,9 +526,11 @@ public class PebbleProtocol extends GBDeviceProtocol {
break;
default:
iconId = PebbleIconID.TIMELINE_CALENDAR;
attributes.add(new Pair<>(3, (Object) calendarEventSpec.description));
attributes.add(new Pair<>(11, (Object) calendarEventSpec.location));
}
return encodeTimelinePin(new UUID(GB_UUID_MASK | calendarEventSpec.type, id), calendarEventSpec.timestamp, (short) calendarEventSpec.durationInSeconds, iconId, calendarEventSpec.title, calendarEventSpec.description);
return encodeTimelinePin(new UUID(GB_UUID_MASK | calendarEventSpec.type, id), calendarEventSpec.timestamp, (short) (calendarEventSpec.durationInSeconds / 60), iconId, attributes);
}
@Override
@ -838,17 +843,34 @@ public class PebbleProtocol extends GBDeviceProtocol {
return buf.array();
}
private byte[] encodeTimelinePin(UUID uuid, int timestamp, short duration, int icon_id, String title, String subtitle) {
private byte[] encodeTimelinePin(UUID uuid, int timestamp, short duration, int icon_id, List<Pair<Integer, Object>> attributes) {
final short TIMELINE_PIN_LENGTH = 46;
//FIXME: dont depend layout on icon :P
byte layout_id = 0x01;
if (icon_id == PebbleIconID.TIMELINE_CALENDAR) {
layout_id = 0x02;
}
icon_id |= 0x80000000;
byte attributes_count = 2;
byte attributes_count = 1;
byte actions_count = 0;
int attributes_length = 10 + title.getBytes().length;
if (subtitle != null && !subtitle.isEmpty()) {
attributes_length += 3 + subtitle.getBytes().length;
attributes_count += 1;
int attributes_length = 7;
for (Pair<Integer, Object> pair : attributes) {
if (pair.first == null || pair.second == null)
continue;
attributes_count++;
if (pair.second instanceof Integer) {
attributes_length += 7;
} else if (pair.second instanceof Byte) {
attributes_length += 4;
} else if (pair.second instanceof String) {
attributes_length += ((String) pair.second).getBytes().length + 3;
} else if (pair.second instanceof byte[]) {
attributes_length += ((byte[]) pair.second).length + 3;
} else {
LOG.warn("unsupported type for timeline attributes: " + pair.second.getClass().toString());
}
}
int pin_length = TIMELINE_PIN_LENGTH + attributes_length;
@ -865,8 +887,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.putShort(duration);
buf.put((byte) 0x02); // type (0x02 = pin)
buf.putShort((short) 0x0001); // flags 0x0001 = ?
buf.put((byte) 0x01); // layout was (0x02 = pin?), 0x01 needed for subtitle but seems to do no harm if there isn't one
buf.put(layout_id); // layout was (0x02 = pin?), 0x01 needed for subtitle but seems to do no harm if there isn't one
buf.putShort((short) attributes_length); // total length of all attributes and actions in bytes
buf.put(attributes_count);
buf.put(actions_count);
@ -874,13 +895,24 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.put((byte) 4); // icon
buf.putShort((short) 4); // length of int
buf.putInt(icon_id);
buf.put((byte) 1); // title
buf.putShort((short) title.getBytes().length);
buf.put(title.getBytes());
if (subtitle != null && !subtitle.isEmpty()) {
buf.put((byte) 2); //subtitle
buf.putShort((short) subtitle.getBytes().length);
buf.put(subtitle.getBytes());
for (Pair<Integer, Object> pair : attributes) {
if (pair.first == null || pair.second == null)
continue;
buf.put(pair.first.byteValue());
if (pair.second instanceof Integer) {
buf.putShort((short) 4);
buf.putInt(((Integer) pair.second));
} else if (pair.second instanceof Byte) {
buf.putShort((short) 1);
buf.put((Byte) pair.second);
} else if (pair.second instanceof String) {
buf.putShort((short) ((String) pair.second).getBytes().length);
buf.put(((String) pair.second).getBytes());
} else if (pair.second instanceof byte[]) {
buf.putShort((short) ((byte[]) pair.second).length);
buf.put((byte[]) pair.second);
}
}
return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_PIN, buf.array());
@ -2341,13 +2373,30 @@ public class PebbleProtocol extends GBDeviceProtocol {
GBDeviceEventSendBytes sendBytes = new GBDeviceEventSendBytes();
if (command == 0x01) { //session setup
sendBytes.encodedBytes = null;
} else if (command == 0x02) { //dictation result
int replLenght = 7;
byte replStatus = 5; // 5 = disabled, change to 0 to send success
ByteBuffer repl = ByteBuffer.allocate(LENGTH_PREFIX + replLenght);
repl.order(ByteOrder.BIG_ENDIAN);
repl.putShort((short) replLenght);
repl.putShort(ENDPOINT_VOICECONTROL);
repl.put(command);
repl.putInt(flags);
repl.put(session_type);
repl.put(replStatus);
sendBytes.encodedBytes = repl.array();
} else if (command == 0x02) { //dictation result (possibly it is something we send, not something we receive)
sendBytes.encodedBytes = null;
}
return sendBytes;
}
private GBDeviceEvent decodeAudioStream(ByteBuffer buf) {
return null;
}
@Override
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
ByteBuffer buf = ByteBuffer.wrap(responseData);
@ -2607,11 +2656,13 @@ public class PebbleProtocol extends GBDeviceProtocol {
case ENDPOINT_APPLOGS:
decodeAppLogs(buf);
break;
// case ENDPOINT_VOICECONTROL:
// devEvts = new GBDeviceEvent[]{decodeVoiceControl(buf)};
// case ENDPOINT_AUDIOSTREAM:
// LOG.debug(GB.hexdump(responseData, 0, responseData.length));
// break;
case ENDPOINT_VOICECONTROL:
devEvts = new GBDeviceEvent[]{decodeVoiceControl(buf)};
break;
case ENDPOINT_AUDIOSTREAM:
devEvts = new GBDeviceEvent[]{decodeAudioStream(buf)};
// LOG.debug("AUDIOSTREAM DATA: " + GB.hexdump(responseData, 4, length));
break;
default:
break;
}

View File

@ -148,6 +148,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
}
}
if (reconnect()) {
super.onDeleteNotification(notificationSpec.id); //update notification hack
super.onNotification(notificationSpec);
}
}

View File

@ -124,11 +124,6 @@ public class VibratissimoSupport extends AbstractBTLEDeviceSupport {
return true;
}
@Override
public void pair() {
}
private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
LOG.warn("Device info: " + info);
versionCmd.hwVersion = info.getHardwareRevision();

View File

@ -68,12 +68,6 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
}
}
@Override
public void pair() {
// Default implementation does no manual pairing, use the Android
// pairing dialog instead.
}
/**
* Lazily creates and returns the GBDeviceProtocol instance to be used.
*/

View File

@ -71,16 +71,17 @@ public class CheckSums {
}
}
public static byte[] readAll(InputStream in, long maxLen) throws IOException {
// copy&paste of FileUtils.readAll() to have it free from Android dependencies
private static byte[] readAll(InputStream in, long maxLen) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(8192, in.available()));
byte[] buf = new byte[8192];
int read = 0;
int read;
long totalRead = 0;
while ((read = in.read(buf)) > 0) {
out.write(buf, 0, read);
totalRead += read;
if (totalRead > maxLen) {
throw new IOException("Too much data to read into memory. Got already " + totalRead + buf);
throw new IOException("Too much data to read into memory. Got already " + totalRead);
}
}
return out.toByteArray();

View File

@ -54,7 +54,7 @@ public class DateTimeUtils {
DurationFormatter df = DurationFormatter.Builder.SYMBOLS
.maximum(TimeUnit.DAYS)
.minimum(TimeUnit.MINUTES)
.suppressZeros(DurationFormatter.SuppressZeros.LEADING)
.suppressZeros(DurationFormatter.SuppressZeros.LEADING, DurationFormatter.SuppressZeros.TRAILING)
.maximumAmountOfUnitsToShow(2)
.build();
return df.format(duration, unit);

View File

@ -38,6 +38,7 @@ import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
public class FileUtils {
// Don't use slf4j here -- would be a bootstrapping problem
@ -54,7 +55,9 @@ public class FileUtils {
if (!sourceFile.exists()) {
throw new IOException("Does not exist: " + sourceFile.getAbsolutePath());
}
copyFile(new FileInputStream(sourceFile), new FileOutputStream(destFile));
try (FileInputStream in = new FileInputStream(sourceFile); FileOutputStream out = new FileOutputStream(destFile)) {
copyFile(in, out);
}
}
private static void copyFile(FileInputStream sourceStream, FileOutputStream destStream) throws IOException {
@ -207,9 +210,11 @@ public class FileUtils {
// the first directory is also the primary external storage, i.e. the same as Environment.getExternalFilesDir()
// TODO: check the mount state of *all* dirs when switching to later API level
if (i == 0 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
GB.log("ignoring unmounted external storage dir: " + dir, GB.INFO, null);
continue;
if (!GBEnvironment.env().isLocalTest()) { // don't do this with robolectric
if (i == 0 && !Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
GB.log("ignoring unmounted external storage dir: " + dir, GB.INFO, null);
continue;
}
}
result.add(dir); // add last
}
@ -229,13 +234,13 @@ public class FileUtils {
public static byte[] readAll(InputStream in, long maxLen) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(8192, in.available()));
byte[] buf = new byte[8192];
int read = 0;
int read;
long totalRead = 0;
while ((read = in.read(buf)) > 0) {
out.write(buf, 0, read);
totalRead += read;
if (totalRead > maxLen) {
throw new IOException("Too much data to read into memory. Got already " + totalRead + buf);
throw new IOException("Too much data to read into memory. Got already " + totalRead);
}
}
return out.toByteArray();

View File

@ -60,12 +60,8 @@ public class GB {
public static final String DISPLAY_MESSAGE_MESSAGE = "message";
public static final String DISPLAY_MESSAGE_DURATION = "duration";
public static final String DISPLAY_MESSAGE_SEVERITY = "severity";
public static GBEnvironment environment;
public static Notification createNotification(String text, boolean connected, Context context) {
if (env().isLocalTest()) {
return null;
}
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -80,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);
@ -227,7 +223,7 @@ public class GB {
*/
public static void toast(final Context context, final String message, final int displayTime, final int severity, final Throwable ex) {
log(message, severity, ex); // log immediately, not delayed
if (env().isLocalTest()) {
if (GBEnvironment.env().isLocalTest()) {
return;
}
Looper mainLooper = Looper.getMainLooper();
@ -272,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)
@ -358,15 +354,15 @@ public class GB {
}
public static void updateBatteryNotification(String text, String bigText, Context context) {
if (env().isLocalTest()) {
if (GBEnvironment.env().isLocalTest()) {
return;
}
Notification notification = createBatteryNotification(text, bigText, context);
updateNotification(notification, NOTIFICATION_ID_LOW_BATTERY, context);
}
public static GBEnvironment env() {
return environment;
public static void removeBatteryNotification(Context context) {
removeNotification(NOTIFICATION_ID_LOW_BATTERY, context);
}
public static void assertThat(boolean condition, String errorMessage) {

View File

@ -20,7 +20,7 @@ import java.text.ParseException;
import java.util.Date;
public class GBPrefs {
public static final String PACKAGE_BLACKLIST = "package_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;
@ -61,7 +61,7 @@ public class GBPrefs {
}
}
public int getUserSex() {
public int getUserGender() {
return 0;
}
}

View File

@ -0,0 +1,147 @@
/* Copyright (C) 2017 Alberto, 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.util;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
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 {
private static final String BOOLEAN = Boolean.class.getSimpleName();
private static final String FLOAT = Float.class.getSimpleName();
private static final String INTEGER = Integer.class.getSimpleName();
private static final String LONG = Long.class.getSimpleName();
private static final String STRING = String.class.getSimpleName();
private static final String HASHSET = HashSet.class.getSimpleName();
private static final String NAME = "name";
private static final String PREFERENCES = "preferences";
public static void exportToFile(SharedPreferences sharedPreferences, File outFile,
Set<String> doNotExport) throws IOException {
export(sharedPreferences, new FileWriter(outFile), doNotExport);
}
public static void export(SharedPreferences sharedPreferences, Writer writer,
Set<String> doNotExport) throws IOException {
XmlSerializer serializer = Xml.newSerializer();
serializer.setOutput(writer);
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
serializer.startDocument("UTF-8", true);
serializer.startTag("", PREFERENCES);
for (Map.Entry<String, ?> entry : sharedPreferences.getAll().entrySet()) {
String key = entry.getKey();
if (doNotExport != null && doNotExport.contains(key)) continue;
Object valueObject = entry.getValue();
// Skip this entry if the value is null;
if (valueObject == null) continue;
String valueType = valueObject.getClass().getSimpleName();
String value = valueObject.toString();
serializer.startTag("", valueType);
serializer.attribute("", NAME, key);
serializer.text(value);
serializer.endTag("", valueType);
}
serializer.endTag("", PREFERENCES);
serializer.endDocument();
writer.close();
}
public static boolean importFromFile(SharedPreferences sharedPreferences, File inFile)
throws Exception {
return importFromReader(sharedPreferences, new FileReader(inFile));
}
/**
*
* @param sharedPreferences
* @param in
* @return
* @throws Exception
*/
public static boolean importFromReader(SharedPreferences sharedPreferences, Reader in)
throws Exception {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
XmlPullParser parser = Xml.newPullParser();
parser.setInput(in);
int eventType = parser.getEventType();
String name = null;
String key = null;
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
name = parser.getName();
key = parser.getAttributeValue("", NAME);
break;
case XmlPullParser.TEXT:
// The parser is reading text outside an element if name is null,
// so simply ignore this text part (which is usually something like '\n')
if (name == null) break;
String text = parser.getText();
if (BOOLEAN.equals(name)) {
editor.putBoolean(key, Boolean.parseBoolean(text));
} else if (FLOAT.equals(name)) {
editor.putFloat(key, Float.parseFloat(text));
} else if (INTEGER.equals(name)) {
editor.putInt(key, Integer.parseInt(text));
} else if (LONG.equals(name)) {
editor.putLong(key, Long.parseLong(text));
} else if (STRING.equals(name)) {
editor.putString(key, text);
} else if (HASHSET.equals(name)) {
if (key.equals(GBPrefs.PACKAGE_BLACKLIST)) {
Set<String> blacklist = new HashSet<>();
text=text.replace("[","").replace("]","");
for (int z=0;z<text.split(",").length;z++){
blacklist.add(text.split(",")[z].trim());
}
GBApplication.setBlackList(blacklist);
}
} else if (!PREFERENCES.equals(name)) {
throw new Exception("Unkown type " + name);
}
break;
case XmlPullParser.END_TAG:
name = null;
break;
}
eventType = parser.next();
}
return editor.commit();
}
}

View File

@ -79,6 +79,7 @@ public class UriHelper {
* Opens a stream to read the contents of the uri.
* Note: the caller has to close the stream after usage.
* Every invocation of this method will open a new stream.
* FIXME: make sure that every caller actually closes the returned stream!
* @throws FileNotFoundException
*/
@NonNull
@ -127,27 +128,31 @@ public class UriHelper {
if (cursor == null) {
throw new IOException("Unable to query metadata for: " + uri);
}
if (cursor.moveToFirst()) {
int name_index = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
if (name_index == -1) {
throw new IOException("Unable to retrieve name for: " + uri);
}
int size_index = cursor.getColumnIndex(MediaStore.MediaColumns.SIZE);
if (size_index == -1) {
throw new IOException("Unable to retrieve size for: " + uri);
}
try {
fileName = cursor.getString(name_index);
if (fileName == null) {
try {
if (cursor.moveToFirst()) {
int name_index = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
if (name_index == -1) {
throw new IOException("Unable to retrieve name for: " + uri);
}
fileSize = cursor.getLong(size_index);
if (fileSize < 0) {
int size_index = cursor.getColumnIndex(MediaStore.MediaColumns.SIZE);
if (size_index == -1) {
throw new IOException("Unable to retrieve size for: " + uri);
}
} catch (Exception ex) {
throw new IOException("Unable to retrieve metadata for: " + uri + ": " + ex.getMessage());
try {
fileName = cursor.getString(name_index);
if (fileName == null) {
throw new IOException("Unable to retrieve name for: " + uri);
}
fileSize = cursor.getLong(size_index);
if (fileSize < 0) {
throw new IOException("Unable to retrieve size for: " + uri);
}
} catch (Exception ex) {
throw new IOException("Unable to retrieve metadata for: " + uri + ": " + ex.getMessage());
}
}
} finally {
cursor.close();
}
} else if (ContentResolver.SCHEME_FILE.equals(uriScheme)) {
file = new File(uri.getPath());

View File

@ -61,4 +61,9 @@ public class Version implements Comparable<Version> {
return false;
return this.compareTo((Version) that) == 0;
}
@Override
public int hashCode() {
return version.hashCode();
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorAccent" android:state_checked="true" />
<item android:color="?attr/textColorPrimary" />
</selector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

View File

@ -1,214 +1,114 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.AlarmDetails">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_smart_wakeup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_smart_wakeup"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/alarm_smart_wakeup"
android:id="@+id/alarm_label_smart_wakeup"
android:labelFor="@id/alarm_cb_smart_wakeup" />
</LinearLayout>
android:layout_marginStart="4dp"
android:drawableStart="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_smart_wakeup"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TimePicker
android:id="@+id/alarm_time_picker"
android:timePickerMode="clock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp" />
<LinearLayout
android:orientation="horizontal"
android:id="@+id/dowSelector"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:id="@+id/dowSelector">
android:orientation="horizontal">
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_monday"
android:layout_width="wrap_content"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_mon_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_mon"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|bottom"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_mon_short"
android:id="@+id/alarm_label_cb_mon"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_mon"
android:gravity="center_horizontal|top"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_tuesday"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_tue_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_tue"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|bottom" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_tue_short"
android:id="@+id/alarm_label_cb_tue"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_tue"
android:gravity="center_horizontal|top" />
</LinearLayout>
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_wednesday"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_wed_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_wed"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|bottom"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_wed_short"
android:id="@+id/alarm_label_cb_wed"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_wed"
android:gravity="center_horizontal|top" />
</LinearLayout>
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_thursday"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_thu_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_thu"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|bottom" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_thu_short"
android:id="@+id/alarm_label_cb_thu"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_thu"
android:gravity="center_horizontal|top" />
</LinearLayout>
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_friday"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_fri_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_fri"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|bottom"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_fri_short"
android:id="@+id/alarm_label_cb_fri"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_fri"
android:gravity="center_horizontal|top" />
</LinearLayout>
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_saturday"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1">
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_sat_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_sat"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal|bottom"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_sat_short"
android:id="@+id/alarm_label_cb_sat"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_sat"
android:gravity="center_horizontal|top" />
</LinearLayout>
<LinearLayout
android:orientation="vertical"
<android.support.v7.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_sunday"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/alarm_cb_sun"
android:layout_gravity="center_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_sun_short"
android:id="@+id/alarm_label_cb_sun"
android:layout_gravity="center_horizontal"
android:labelFor="@id/alarm_cb_sun"
android:gravity="center_horizontal|top" />
</LinearLayout>
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_sun_short"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
</LinearLayout>

View File

@ -1,12 +1,16 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.AndroidPairingActivity">
<TextView android:text="@string/android_pairing_hint" android:layout_width="wrap_content"
<TextView
android:text="@string/android_pairing_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>

View File

@ -1,16 +1,25 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.AppBlacklistActivity">
<ListView
android:layout_width="wrap_content"
<android.support.v7.widget.SearchView
android:id="@+id/appListViewSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/appListView"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
android:paddingEnd="16dp"
android:paddingStart="16dp">
</android.support.v7.widget.SearchView>
<android.support.v7.widget.RecyclerView
android:id="@+id/appListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/appListViewSearch"
android:layout_centerHorizontal="true"
android:divider="@null" />
</RelativeLayout>

View File

@ -14,8 +14,7 @@
android:id="@+id/itemListView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="false">
</ListView>
android:layout_alignParentEnd="false"></ListView>
<TextView
android:id="@+id/infoTextView"
@ -67,8 +66,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/installButton"
android:layout_alignParentEnd="false">
</ListView>
android:layout_alignParentEnd="false"></ListView>
<android.widget.Space
android:layout_width="wrap_content"

View File

@ -5,7 +5,10 @@
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/appListView"
android:layout_width="match_parent"
android:layout_height="fill_parent" />
android:id="@+id/appListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:divider="@null" />
</RelativeLayout>

View File

@ -1,56 +1,59 @@
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_swipe_layout"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity"
android:paddingLeft="0px"
android:paddingRight="0px"
android:paddingTop="0px"
android:paddingBottom="0px"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent"
android:orientation="vertical"
android:id="@+id/charts_main_layout"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity">
<LinearLayout
android:id="@+id/charts_date_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/charts_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="&lt;" />
<TextView
android:id="@+id/charts_text_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:text="Today"
/>
android:id="@+id/charts_main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/charts_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=">" />
</LinearLayout>
<android.support.v4.view.ViewPager android:id="@+id/charts_pager"
android:layout_width="match_parent" android:layout_height="match_parent"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity">
<android.support.v4.view.PagerTabStrip
android:id="@+id/charts_pagerTabStrip"
<android.support.v4.view.ViewPager
android:id="@+id/charts_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity">
<android.support.design.widget.TabLayout
android:id="@+id/charts_pagerTabStrip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:tabMode="scrollable" />
</android.support.v4.view.ViewPager>
<LinearLayout
android:id="@+id/charts_date_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
</android.support.v4.view.ViewPager>
android:gravity="fill_horizontal"
android:orientation="horizontal">
<Button
android:id="@+id/charts_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="&lt;" />
</LinearLayout>
<TextView
android:id="@+id/charts_text_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_weight="1"
android:text="Today" />
<Button
android:id="@+id/charts_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=">" />
</LinearLayout>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/charts_duration_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
android:layout_height="wrap_content"></TextView>
</LinearLayout>

View File

@ -1,14 +1,17 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:fitsSystemWindows="true"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms">
<ListView
android:descendantFocusability="blocksDescendants"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:divider="@null"
android:id="@+id/alarm_list" />
</FrameLayout>
</RelativeLayout>

View File

@ -30,11 +30,11 @@
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_add_white"
app:srcCompat="@drawable/ic_add"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:layout_marginBottom="30dp"
android:layout_marginRight="10dp"/>
android:layout_marginEnd="10dp" />
</android.support.design.widget.CoordinatorLayout>

View File

@ -1,6 +1,8 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"

View File

@ -3,14 +3,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingBottom="0px"
android:paddingLeft="0px"
android:paddingRight="0px"
android:paddingTop="0px"
tools:context=".activities.appmanager.AppManagerActivity">
<LinearLayout
android:id="@+id/charts_main_layout"
android:id="@+id/appmanager_main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -21,14 +17,15 @@
android:layout_height="match_parent"
tools:context=".activities.appmanager.AppManagerActivity">
<android.support.v4.view.PagerTabStrip
<android.support.design.widget.TabLayout
android:id="@+id/charts_pagerTabStrip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
android:layout_gravity="top" />
</android.support.v4.view.ViewPager>
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
@ -36,10 +33,10 @@
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_gravity="bottom|end"
android:src="@drawable/ic_add_white"
app:srcCompat="@drawable/ic_add"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp" />
android:layout_marginBottom="30dp"
android:layout_marginEnd="10dp" />
</android.widget.RelativeLayout>

View File

@ -1,12 +1,16 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPairingActivity">
<TextView android:text="@string/pairing" android:layout_width="wrap_content"
<TextView
android:text="@string/pairing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/miband_pair_message" />

View File

@ -1,12 +1,16 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebblePairingActivity">
<TextView android:text="@string/pairing" android:layout_width="wrap_content"
<TextView
android:text="@string/pairing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/pebble_pair_message" />

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