Compare commits

..

No commits in common. "master" and "0.5.4" have entirely different histories.

640 changed files with 6765 additions and 57222 deletions

View File

@ -1,19 +0,0 @@
#### Before opening an issue please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
#### Your issue is:
*In case of a bug, do not forget to attach logs!*
#### Your wearable device is:
*Please specify model and firmware version if possible*
#### Your android version is:
#### Your Gadgetbridge version is:
*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.*

4
.gitignore vendored
View File

@ -27,7 +27,3 @@ proguard/
.idea
*.iml
MPChartLib
fw.dirs

View File

@ -1,25 +1,16 @@
language: android
jdk:
- oraclejdk8
# disabled -- we now set sourceCompatibility and targetCompatibility appropriately
# - oraclejdk7
env:
- GRADLE_OPTS="-XX:MaxPermSize=256m"
android:
components:
# Uncomment the lines below if you want to
# use the latest revision of Android SDK Tools
# - platform-tools
- tools
# - tools
# The BuildTools version used by your project
- build-tools-25.0.2
- build-tools-23.0.0
# The SDK version used to compile your project
- android-25
- android-23
# Additional components
- extra-android-m2repository
@ -29,5 +20,3 @@ android:
# if you need to run emulator(s) during your tests
#- sys-img-armeabi-v7a-android-19
#- sys-img-x86-android-17
script: ./gradlew build connectedCheck --stacktrace

9
.tx/config Normal file
View File

@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[gadgetbridge.strings]
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en_US
deviceType = ANDROID
minimum_perc = 50

View File

@ -1,589 +1,12 @@
### Changelog
###Changelog
#### Version 0.21.5
* Mi2/Bip: Support setting distance units (metric/imperial)
#### Version 0.21.4
* Mi2/Bip: Fix sleep detection for newer firmwares
* Mi2/Bip: Fix ancient bug resulting in wrong activity data at the beginning in diagrams and aggregate data
* No.1 F1: Support setting time format and distance units (metric/imperial)
* Pebble: Support setting distance units to miles for Health (need to reactivate Health in App Manager after toggling)
* HPlus: Make changing distance unit system effective immediately on toggling
#### Version 0.21.3
* Amazfit Bip: Auto-switch language on connect (English, Simplified Chinese, Traditional Chinese), requires FW 0.0.9.14+
#### Version 0.21.2
* Amazfit Bip: Support flashing CEP and ALM files for AGPS
* Amazfit Bip: Initial experimental support for fetching logs from the watch
* Mi2/Bip: Send user info to the device (fixes calories and distance display)
* Mi2/Bip: Fix firmware update progressbar being stuck at the end
* Pebble/Bip: Support more notification icons
* Pebble: Automatically determine color for unknown notifications on Pebble Time
#### Version 0.21.1
* Initial support for EXRIZU K8 (HPLus variant)
* Amazfit Bip: fix long messages not being displayed at all
* Mi Band 2: Support multiple button actions
* NO.1 F1: Fetch sleep data
* NO.1 F1: Heart rate support
* Pebble: Support controlling the current active media playback application
* Fix suspended activities coming to front when rotating the screen
#### Version 0.21.0
* Initial NO.1 F1 support
* Initial Teclast H30 support
* Amazfit Bip: Display GPS firmware version
* Amazfit Bip: Fix E-Mail notifications
* Amazfit Bip: Fix call notification with unknown caller
* Amazfit Bip: Fix crash when weather is updated and device reconnecting
* Mi2/Bip: Fix crash when synchronizing calendar to alarms
* Pebble: Fix crash when takeing screenshots on Android 8.0 (Oreo)
* Pebble: Support some google app icons
* Pebble: try to support spotify
* Mi Band 2: Support configurable button actions
* Fix language being reset to system default
#### Version 0.20.2
* Amazfit Bip: Various fixes regarding weather, add condition string support for FW 0.0.8.74
* Amazfit Bip: enable caller display in later firmwares
* Amazfit Bip: initial firmware update support (EXPERIMENTAL, AT YOUR OWN RISK)
* Re-enable improved speed zones tab
* Probably fix crash with certain music players
* Improve theme and add changelog icon
#### Version 0.20.1
* Amazfit Bip: Support icons and text body for notifications
* Mi Band: Fix setting smart alarms
#### Version 0.20.0
* Inital Amazfit Bip support (WIP)
* Various theming fixes
* Add workaround for blacklist not properly persisting
* Handle resetting language to default properly
* Pebble: Pass booleans from Javascript Appmessage correctly
* Pebble: Make local configuration pages work on most recent webview implementation
* Pebble: Allow to blacklist calendars
* Add Greek and German transliteration support
* Various visual improvements to charts
#### Version 0.19.4
* Replace or relicense CC-NC licensed icons to satisfy F-Droid
* Mi Band 2: Make infos to display on the Band configurable
* Mi Band 2: Support wrist rotation to switch info setting
* Mi Band 2: Support goal notification setting
* Mi Band 2: Support do not disturb setting
* Mi Band 2: Support inactivity warning setting
#### Version 0.19.3
* Pebble: Fix crash when calendar access permission has been denied
* Pebble: Fix wrong timestamps with Morpheuz running on Firmware >=3
* Mi Band 2: Improve reliability when fetching activity data
* HPlus: Fix intensity calculation without continuous connectivity
* HPlus: Fix Unicode handling
* HPlus: Initial not work detection
* Fix memory leak
* Only show Realtime Chart on devices supporting it
#### Version 0.19.2
* Pebble: Fix recurring calendar events only appearing once per week
* HPlus: Fix crash when receiving calls without phone number
* HPlus: Detect unicode support on Zeband Plus
* No longer quit Gadgetbridge when bluetooth gets turned off
#### Version 0.19.1
* Fix crash at startup
* HPlus: Improve reconnection to device
* Improve transliteration
#### Version 0.19.0
* 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
* Fix bug that caused the same value in weekly charts for every day on Android 6 and older
#### Version 0.18.2
* Mi Band 2: Fix crash on "chat" or "social network" text notification (#603)
#### 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
* All new GUI for the control center
* Add Portuguese pt_PT and pt_BR translations
* Add Czech translation
* Add Hebrew translation and transliteration
* Consistently display device specific icons already during discovery
* Add sleep chart displaying the last week of sleep
* Huge speedup for weekly charts when changing days
* Drop support for importing pre Gadgetbridge 0.12.0 database
* Pebble: allow configuration web pages (clay) to access device location
* Mi Band 2: Initial support for text notifications, caller ID, and icons (requires font installation) (#560)
* Mi Band 2: Support for flashing Mili_pro.ft* font files
* Mi Band 2: Improved firmware/font updated
* Mi Band 2: Set 12h/24h time format, following the Android configuration (#573)
* Improved BLE discovery and connectivity
#### 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
* 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)
* Fixes for cyrillic transliteration
* Pebble: Implement notification privacy modes
* Pebble: Support weather for Obisdian watchface
* Pebble: add a dev option to always and immediately ACK PebbleKit messages to the watch
* HPlus: Support alarms
* HPlus: Fix time and date sync and time format (12/24)
* HPlus: Add device specific preferences and icon
* HPlus: Support for Makibes F68
#### 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
* Pebble: Fix temperature unit in Timestyle Pebble watchface
* Add optional Cyrillic transliteration (for devices lacking the font)
#### 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
* 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
* Pebble: Support for build-in weather system app (FW 4.x)
* Pebble: Add weather support for various watchfaces
* Pebble: Add option to disable call display
* Pebble: Add option to automatically delete notifications that got dismissed on the phone
* Pebble: Bugfix for some PebbleKit enabled 3rd party apps (TCW and maybe other)
* Pebble 2/LE: Improve reliablitly and transfer speed
* HPlus: Improved discovery and pairing
* HPlus: Improved notifications (display + vibration)
* HPlus: Synchronize time and date
* HPlus: Display firmware version and battery charge
* HPlus: Near real time Heart rate measurement
* HPlus: Experimental synchronization of activity data (only sleep, steps and intensity)
* HPlus: Fix some disconnection issues
#### 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
* Charts (Live Activity): Fix axis labels color in dark theme
* Mi Band: Fix ginormous step count when using Live Activity
* Mi Band: Improved performance during activity sync
* Mi Band 2: Fix activity data missing after doing manual hr measurements or live activity
* Support sharing firmwares/watchapps/watchfaces to Gadgetbridge
* Support for the "Subsonic" music player (#474)
#### Version 0.15.2
* Mi Band: Fix crash with unknown notification sources
#### 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
* 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
* Pebble: notification icons for more apps
* 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
* 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
* 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
* Pebble 2: Fix a bug where the Pebble got disconnected by other unrelated LE devices
#### 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)
* Pebble: Further improve compatibility for watchface configuration
* 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
* 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
* Mi Band 2: support for heart rate measurement during sleep
* Mi Band 2: configuration option to activate the display on lift
* 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
* 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
* 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
* 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
* Mi Band 2: Initial support for heart rate readings (Debug activity only)
* Mi Band 2: Support disabled alarms
* Attempt to fix spurious device discovery problems
* Correctly recognize Toffeed, Slimsocial and MaterialFBook as facebook notification sources
#### 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
* 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
* 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
* Possibly fix logging to file on certain devices (#406)
* Mi Band 2: Possibly fix weird connection interdependency between Mi 1 and 2 (#323)
* Mi Band 1S: Whitelist firmware 4.16.4.22
* Mi Band: try application level pairing again, in order to support data sharing with Mi Fit (#250)
* 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
* 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
* 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
* 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
* Initial working Mi Band 2 support (only notifications, no activity and heart rate support)
* Experimental support for Vibratissimo devices
#### 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)
* 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)
* 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
* In Control center, new menu point to launch the new "Database management" activity
* Pebble: Support for Pebble Health on Firmware 4.0
* 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
* Mi Band: support for devices that cannot pair with the band (#349)
#### 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
* Fix activity fetching getting stuck when double tapping (#333)
* Mi Band: in the Device Discovery activity, do not display devices that are already paired
* Mi Band: only allow automatic reconnection on disconnect when the device was previously fully connected
* Mi Band: fix a rare crash when reading data fails due to Bluetooth problems
* Mi Band: log full activity sample to help deciphering activity kinds (#341)
* Mi Band 2: improved discovery mechanism to not rely on MAC addresses (#323)
* 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
* 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
* 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
* 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)
* Mi Band: support for shifting the device time by N hours (for people who sleep at daytime)
* Mi Band: initial and untested support for Mi Band 2
* Allow setting the application language
#### 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
* Fixes for enabling logging at without restarting Gadgetbridge
* Re-enable device paring activity on Android 6 (BLE scanning needs the location preference)
* Display device address in device info
#### 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
* Pebble: enable pbw cache and watchface configuration for Firmware 2.x
* Pebble: allow enabling of Pebble Health without "untested features" being enabled
* Pebble: fix music information being messed up
* Honour "Do Not Disturb" for phone calls and SMS
#### 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
* Mi Band: initial experimental and probably broken support for Amazfit
* 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
* 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
* Several UI Improvements
* Easier First-time setup by using a FAB
* Optional Dark Theme
* Notification App Blacklist is now sorted
* Gadgetbridge Icon in the notification bar displays connection state
* Logging is now configurable without restart
* Mi Band 1S: Initial live heartrate tracking
* Fix certain crash in charts activity on slower devices (#277)
#### 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)
* Fix crash in charts activities when changing the date, quickly (#277)
* Mi Band: preference to enable heart rate measurement during sleep (#232, thanks computerlyrik!)
* Mi Band: display measured heart rate in charts (#232)
* 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
* 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
* Mi Band: Fix update of second (HR) firmware on Mi1S (#234)
* Fix ordering issue of device infos being displayed
#### 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
* 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
* Mi Band: Display HR FW for 1S
* FW and HW versions are only displayed after tapping on the "info" button in Control Center
* 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
* 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
* 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
* 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)
* Pebble: Fix wrong(previous) contact being displayed on the pebble
* Mi Band: improvements to pairing and connecting
* Fix a problem related to shared preferences storage of activity settings
* Very basic support Android 6 runtime permission
* Fix layout of the alarms activity
#### 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
* 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
* 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
* Mi Band: Fix discovery of Mi Band 1S
* Add a confirmation dialog when performing a db import
* Sort blacklist by package names
#### 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
* 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)
* Pebble: Allow installing apps compiled with SDK 2.x also on the basalt platform (Time, Time Steel)
* Pebble: Fix decoding strings in appmessages from the pebble (fixes sending SMS from "Dialer for Pebble")
* Pebble: Support incoming reconnections when device returns from "Airplane Mode" or "Stand-By Mode"
* Pebble: Fix crash when turning off Bluetooth when connected on Android 6.0
* Mi Band: reserve some alarm slots for alerting when upcoming events begin. NB: the band will vibrate at the start time of the event, android reminders are ignored
* Mi Band: Display unique devices Names, not just "MI"
* Some new and updated icons
#### 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
* Pebble: Correct setting the timezone on firmware 3.x (pebble expects the "ID" eg. Europe/Berlin)
* Pebble: Show correct icon for activity tracker and watchfaces in app installer (language and fw icons still missing)
* Pebble: Fix crash when trying to install files though a file manager which are located inside the pbw-cache on firmware 3.x
* Support for deleting all activity data (in the 'Debug' screen)
* Don't pop up the virtual keyboard when entering the Debug screen
* Remove all pending notifications on quit
* 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
* 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)
* Pebble: fix crash on firmware 3.x when pebble requests a pbw that is not in Gadgetbridge's cache
+ Treat Signal notifications as chat notifications
* Fix crash when contacts cannot be read on Android 6.0 (non-granted permissions)
#### 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
* 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
* 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
* 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)
* Mi Band: Attempt at fixing problem with never finishing activity data fetching (#141, #142)
* Register/unregister BroadcastReceivers instead of enabling/disabling them with PackageManager (#134)
(should fix disconnection because the service is being killed)
#### 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
* 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
* 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
* 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)
* Treat SMS as generic notification if set to "never" (can be blacklisted there also if desired)
* Treat Conversations messages as chat messages, even if arrived via Pebble Intents (nice icon for Pebble FW 3.x)
* 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
* Mi Band: allow the transfer of activity data without clearing MiBand's memory
####Version 0.5.4
* Miband: 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
@ -591,28 +14,28 @@
* 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%
* Miband: show a notification when the battery is below 10%
* Graphs are now using the same theme as the rest of the application
* Graphs now show when the device was not worn by the user (for devices that send this information)
* Remove unused settings option in charts view
* Build target is now Android SDK 23 (Marshmallow)
* Build target is now Android SDK 23 (Marshmellow)
#### 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)
* Pebble: use SMS/EMAIL icons for FW 3.x/Pebble Time
* Pebble: do not throttle notifications
* Support going forward/backwards in time in the activity charts
* Various small bugfixes to the App/FW Installation Activity
* 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)
@ -622,16 +45,16 @@
* 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)
* 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)
* Mi Band: send the wear location (left / right hand) to the device
* 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
* Set Gadgetbridge notification visibility to public, to show the connection status on the lockscreen
####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
* Enhancement to activity graphs: new graphs for sleep data (only last night) accessible swiping right from the main graph
@ -640,24 +63,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)
@ -671,33 +94,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
@ -705,39 +128,39 @@
* 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
* Fix for device list not refreshing when bluetooth gets turned on
* Filter out annoying low battery notifications
* 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
* Added option to start Gadgetbridge and connect automatically when Bluetooth is turned on
* stop service if Bluetooth is turned off
####Version 0.1.2
* Added option to start Gadgetbridge and connect automatically when bluetooth is turned on
* stop service if bluetooth is turned off
* try to reconnect if connection was lost
#### Version 0.1.1
####Version 0.1.1
* Fixed various bugs regarding K-9 Mail notifications.
* "Generic notification support" in Setting now opens Androids "Notification access" dialog.
#### Version 0.1.0
####Version 0.1.0
* Initial release

View File

@ -1,72 +0,0 @@
.. 2>/dev/null
names ()
{
echo -e "\n exit;\n**Contributors (sorted by number of commits):**\n";
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 ()
{
{
echo ".. 2>/dev/null";
declare -f names | sed -e 's/^[[:space:]]*/ /';
declare -f quine | sed -e 's/^[[:space:]]*/ /';
echo -e " quine\n";
names;
echo -e "\nAnd all the Transifex translators, which I cannot automatically list, at the moment.\n\n*To update the contributors list just run this file with bash*"
} > CONTRIBUTORS.rst;
exit
}
quine
exit;
**Contributors (sorted by number of commits):**
* Andreas Shimokawa <shimokawa@fsfe.org>
* Carsten Pfeiffer <cpfeiffer@users.noreply.github.com>
* Daniele Gobbetti <daniele+github@gobbetti.name>
* João Paulo Barraca <jpbarraca@gmail.com>
* ivanovlev <lion.ivanov@gmal.com>
* Julien Pivotto <roidelapluie@inuits.eu>
* Steffen Liebergeld <perl@gmx.org>
* Lem Dulfo <lemuel.dulfo@gmail.com>
* 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>
* Szymon Tomasz Stefanek <s.stefanek@gmail.com>
* Roman Plevka <rplevka@redhat.com>
* rober <rober@prtl.nodomain.net>
* Nicolò Balzarotti <anothersms@gmail.com>
* Natanael Arndt <arndtn@gmail.com>
* Marc Schlaich <marc.schlaich@googlemail.com>
* kevlarcade <kevlarcade@gmail.com>
* Kevin Richter <me@kevinrichter.nl>
* Kasha <kasha_malaga@hotmail.com>
* Ivan <ivan_tizhanin@mail.ru>
* 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>
* andre <andre.buesgen@yahoo.de>
* Alexey Afanasev <avafanasiev@gmail.com>
And all the Transifex translators, which I cannot automatically list, at the moment.
*To update the contributors list just run this file with bash*

View File

@ -1,34 +0,0 @@
## Feature Matrix
| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Amazfit Bip |
|-----------------------------------| ----------|---------------|---------|-----------|-------------|
|Calls Notification | YES | YES | YES | YES | YES |
|Reject Calls | YES | YES | NO | NO | YES |
|Accept Calls | NO(2) | NO(2) | NO | NO | NO(3) |
|Generic Notification | YES | YES | YES | YES | YES |
|Dismiss Notifications on Phone | YES | YES | NO | NO | NO |
|Predefined Replies | YES | YES | NO | NO | NO |
|Voice Replies | N/A | NO(3) | N/A | N/A | N/A |
|Calendar Sync | YES | YES | NO | NO | NO |
|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES |
|Smart alarms | NO(1) | YES | YES | NO | NO |
|Weather | NO(1) | YES | NO | NO | YES |
|Activity Tracking | NO(1) | YES | YES | YES | YES |
|Sleep Tracking | NO(1) | YES | YES | YES | YES |
|HR Tracking | N/A | YES | YES | YES | YES |
|Realtime Activity Tracking | NO | NO | YES | YES | YES |
|Music Control | YES | YES | NO | NO | NO |
|Watchapp/face Installation | YES | YES | NO | NO | NO |
|Firmware Installaton | YES | YES | YES | YES | YES |
|Taking Screenshots | YES | YES | NO | NO | NO |
|Support Android Companion Apps | YES | YES | NO | NO | NO |
(1) Possible via 3rd Party Watchapp
(2) Theoretically possible (works on iOS, would need lot of work)
(3) Possible but not implemented yet
### Notes about Pebble Firmware >=3.0
* Gadgetbridge will keep track of installed watchfaces, but if the Pebble is used with another phone or another app, the information displayed in the app manager can get out of sync since it is impossible to query Firmware >= 3.x for installed apps/watchfaces.

View File

@ -1,2 +0,0 @@
/bin
/build

View File

@ -1,32 +0,0 @@
apply plugin: 'java'
//apply plugin: 'maven'
apply plugin:'application'
archivesBaseName = 'gadgetbridge-daogenerator'
//version = '0.9.2-SNAPSHOT'
dependencies {
// compile 'org.greenrobot:greendao-generator:2.2.0'
// compile project(":DaoGenerator")
compile 'com.github.freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
}
sourceSets {
main {
java {
srcDir 'src'
}
}
}
mainClassName = "nodomain.freeyourgadget.gadgetbridge.daogen.GBDaoGenerator"
task genSources(type: JavaExec) {
main = mainClassName
classpath = sourceSets.main.runtimeClasspath
workingDir = '../'
}
artifacts {
archives jar
}

View File

@ -1,314 +0,0 @@
/*
* Copyright (C) 2011 Markus Junginger, greenrobot (http://greenrobot.de)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
/**
* Generates entities and DAOs for the example project DaoExample.
* Automatically run during build.
*/
public class GBDaoGenerator {
private static final String VALID_FROM_UTC = "validFromUTC";
private static final String VALID_TO_UTC = "validToUTC";
private static final String MAIN_PACKAGE = "nodomain.freeyourgadget.gadgetbridge";
private static final String MODEL_PACKAGE = MAIN_PACKAGE + ".model";
private static final String VALID_BY_DATE = MODEL_PACKAGE + ".ValidByDate";
private static final String OVERRIDE = "@Override";
private static final String SAMPLE_RAW_INTENSITY = "rawIntensity";
private static final String SAMPLE_STEPS = "steps";
private static final String SAMPLE_RAW_KIND = "rawKind";
private static final String SAMPLE_HEART_RATE = "heartRate";
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(17, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
Entity deviceAttributes = addDeviceAttributes(schema);
Entity device = addDevice(schema, deviceAttributes);
// yeah deep shit, has to be here (after device) for db upgrade and column order
// because addDevice adds a property to deviceAttributes also....
deviceAttributes.addStringProperty("volatileIdentifier");
Entity tag = addTag(schema);
Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user);
addMiBandActivitySample(schema, user, device);
addPebbleHealthActivitySample(schema, user, device);
addPebbleHealthActivityKindOverlay(schema, user, device);
addPebbleMisfitActivitySample(schema, user, device);
addPebbleMorpheuzActivitySample(schema, user, device);
addHPlusHealthActivityKindOverlay(schema, user, device);
addHPlusHealthActivitySample(schema, user, device);
addNo1F1ActivitySample(schema, user, device);
addCalendarSyncState(schema, device);
new DaoGenerator().generateAll(schema, "app/src/main/java");
}
private static Entity addTag(Schema schema) {
Entity tag = addEntity(schema, "Tag");
tag.addIdProperty();
tag.addStringProperty("name").notNull();
tag.addStringProperty("description").javaDocGetterAndSetter("An optional description of this tag.");
tag.addLongProperty("userId").notNull();
return tag;
}
private static Entity addActivityDescription(Schema schema, Entity tag, Entity user) {
Entity activityDesc = addEntity(schema, "ActivityDescription");
activityDesc.setJavaDoc("A user may further specify his activity with a detailed description and the help of tags.\nOne or more tags can be added to a given activity range.");
activityDesc.addIdProperty();
activityDesc.addIntProperty(TIMESTAMP_FROM).notNull();
activityDesc.addIntProperty(TIMESTAMP_TO).notNull();
activityDesc.addStringProperty("details").javaDocGetterAndSetter("An optional detailed description, specific to this very activity occurrence.");
Property userId = activityDesc.addLongProperty("userId").notNull().getProperty();
activityDesc.addToOne(user, userId);
Entity activityDescTagLink = addEntity(schema, "ActivityDescTagLink");
activityDescTagLink.addIdProperty();
Property sourceId = activityDescTagLink.addLongProperty("activityDescriptionId").notNull().getProperty();
Property targetId = activityDescTagLink.addLongProperty("tagId").notNull().getProperty();
activityDesc.addToMany(tag, activityDescTagLink, sourceId, targetId);
return activityDesc;
}
private static Entity addUserInfo(Schema schema, Entity userAttributes) {
Entity user = addEntity(schema, "User");
user.addIdProperty();
user.addStringProperty("name").notNull();
user.addDateProperty("birthday").notNull();
user.addIntProperty("gender").notNull();
Property userId = userAttributes.addLongProperty("userId").notNull().getProperty();
// sorted by the from-date, newest first
Property userAttributesSortProperty = getPropertyByName(userAttributes, VALID_FROM_UTC);
user.addToMany(userAttributes, userId).orderDesc(userAttributesSortProperty);
return user;
}
private static Property getPropertyByName(Entity entity, String propertyName) {
for (Property prop : entity.getProperties()) {
if (propertyName.equals(prop.getPropertyName())) {
return prop;
}
}
throw new IllegalStateException("Could not find property " + propertyName + " in entity " + entity.getClassName());
}
private static Entity addUserAttributes(Schema schema) {
// additional properties of a user, which may change during the lifetime of a user
// this allows changing attributes while preserving user identity
Entity userAttributes = addEntity(schema, "UserAttributes");
userAttributes.addIdProperty();
userAttributes.addIntProperty("heightCM").notNull();
userAttributes.addIntProperty("weightKG").notNull();
userAttributes.addIntProperty("sleepGoalHPD").javaDocGetterAndSetter("Desired number of hours of sleep per day.");
userAttributes.addIntProperty("stepsGoalSPD").javaDocGetterAndSetter("Desired number of steps per day.");
addDateValidityTo(userAttributes);
return userAttributes;
}
private static void addDateValidityTo(Entity entity) {
entity.addDateProperty(VALID_FROM_UTC).codeBeforeGetter(OVERRIDE);
entity.addDateProperty(VALID_TO_UTC).codeBeforeGetter(OVERRIDE);
entity.implementsInterface(VALID_BY_DATE);
}
private static Entity addDevice(Schema schema, Entity deviceAttributes) {
Entity device = addEntity(schema, "Device");
device.addIdProperty();
device.addStringProperty("name").notNull();
device.addStringProperty("manufacturer").notNull();
device.addStringProperty("identifier").notNull().unique().javaDocGetterAndSetter("The fixed identifier, i.e. MAC address of the device.");
device.addIntProperty("type").notNull().javaDocGetterAndSetter("The DeviceType key, i.e. the GBDevice's type.");
device.addStringProperty("model").javaDocGetterAndSetter("An optional model, further specifying the kind of device-");
Property deviceId = deviceAttributes.addLongProperty("deviceId").notNull().getProperty();
// sorted by the from-date, newest first
Property deviceAttributesSortProperty = getPropertyByName(deviceAttributes, VALID_FROM_UTC);
device.addToMany(deviceAttributes, deviceId).orderDesc(deviceAttributesSortProperty);
return device;
}
private static Entity addDeviceAttributes(Schema schema) {
Entity deviceAttributes = addEntity(schema, "DeviceAttributes");
deviceAttributes.addIdProperty();
deviceAttributes.addStringProperty("firmwareVersion1").notNull();
deviceAttributes.addStringProperty("firmwareVersion2");
addDateValidityTo(deviceAttributes);
return deviceAttributes;
}
private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "MiBandActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
return activitySample;
}
private static void addHeartRateProperties(Entity activitySample) {
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}
private static Entity addPebbleHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "PebbleHealthActivitySample");
addCommonActivitySampleProperties("AbstractPebbleHealthActivitySample", activitySample, user, device);
activitySample.addByteArrayProperty("rawPebbleHealthData").codeBeforeGetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
return activitySample;
}
private static Entity addPebbleHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
Entity activityOverlay = addEntity(schema, "PebbleHealthActivityOverlay");
activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
activityOverlay.addToOne(device, deviceId);
Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
activityOverlay.addToOne(user, userId);
activityOverlay.addByteArrayProperty("rawPebbleHealthData");
return activityOverlay;
}
private static Entity addPebbleMisfitActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "PebbleMisfitSample");
addCommonActivitySampleProperties("AbstractPebbleMisfitActivitySample", activitySample, user, device);
activitySample.addIntProperty("rawPebbleMisfitSample").notNull().codeBeforeGetter(OVERRIDE);
return activitySample;
}
private static Entity addPebbleMorpheuzActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "PebbleMorpheuzSample");
addCommonActivitySampleProperties("AbstractPebbleMorpheuzActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
return activitySample;
}
private static Entity addHPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "HPlusHealthActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addByteArrayProperty("rawHPlusHealthData");
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
activitySample.addIntProperty("distance");
activitySample.addIntProperty("calories");
return activitySample;
}
private static Entity addHPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
Entity activityOverlay = addEntity(schema, "HPlusHealthActivityOverlay");
activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
activityOverlay.addToOne(device, deviceId);
Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
activityOverlay.addToOne(user, userId);
activityOverlay.addByteArrayProperty("rawHPlusHealthData");
return activityOverlay;
}
private static Entity addNo1F1ActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "No1F1ActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
return activitySample;
}
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
activitySample.setJavaDoc(
"This class represents a sample specific to the device. Values like activity kind or\n" +
"intensity, are device specific. Normalized values can be retrieved through the\n" +
"corresponding {@link SampleProvider}.");
activitySample.addIntProperty("timestamp").notNull().codeBeforeGetterAndSetter(OVERRIDE).primaryKey();
Property deviceId = activitySample.addLongProperty("deviceId").primaryKey().notNull().codeBeforeGetterAndSetter(OVERRIDE).getProperty();
activitySample.addToOne(device, deviceId);
Property userId = activitySample.addLongProperty("userId").notNull().codeBeforeGetterAndSetter(OVERRIDE).getProperty();
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())) {
return prop;
}
}
throw new IllegalArgumentException("Property " + propertyName + " not found in Entity " + entity.getClassName());
}
private static Entity addEntity(Schema schema, String className) {
Entity entity = schema.addEntity(className);
entity.addImport("de.greenrobot.dao.AbstractDao");
return entity;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

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

127
README.md
View File

@ -1,17 +1,9 @@
Gadgetbridge
============
Gadgetbridge is an Android (4.4+) application which will allow you to use your
Pebble, Mi Band, Amazfit Bit and HPlus device (and more) without the vendor's closed source application
and without the need to create an account and transmit any of your data to the
vendor's servers.
[Homepage](https://gadgetbridge.org)
[Blog](https://blog.gadgetbridge.org)
[![Donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Gadgetbridge/donate)
Gadgetbridge is an Android (4.4+) Application which will allow you to use your
Pebble or Mi Band without the vendor's closed source application and without the
need to create an account and transmit any of your data to the vendor's servers.
[![Build](https://travis-ci.org/Freeyourgadget/Gadgetbridge.svg?branch=master)](https://travis-ci.org/Freeyourgadget/Gadgetbridge)
@ -19,99 +11,102 @@ vendor's servers.
[![Gadgetbridge on F-Droid](/Get_it_on_F-Droid.svg.png?raw=true "Download from F-Droid")](https://f-droid.org/repository/browse/?fdid=nodomain.freeyourgadget.gadgetbridge)
[List of changes](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/CHANGELOG.md)
[List of changes](CHANGELOG.md)
## Supported Devices
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 (add the device from within Gadgetbridge!) [Wiki section about pebble](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble), most parts apply to Pebble 2 as well
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki section about mi band](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), some parts apply to mi band 2 as well
* Amazfit Bip (WIP) [Wiki section about the Amazfit Bip](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* HPlus Devices (e.g. ZeBand) [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* Teclast H30 (WIP)
* NO.1 F1 (WIP)
* Liveview
* Vibratissimo (experimental)
## Features (Pebble)
## Features
* Incoming calls notification and display (caller, phone number)
* Outgoing call display
* Reject/hangup calls
* SMS notification (sender, body)
* K-9 Mail notification support (sender, subject, preview)
* Support for generic notifications (above filtered out)
* Dismiss individial notifications or open corresponding app on phone from the action menu (generic notifications)
* Dismiss all notifications from the action menu (non-generic notifications)
* Music playback info (artist, album, track). Apollo and CM 12.1 Music App supported.
* Music control: play/pause, next track, previous track, volume up, volume down
* List and remove installed apps/watchfaces
* Install .pbw files
* Install firmware from .pbz files
* Take and share screenshots from the Pebble's screen
* Morpheuz sleep data syncronization (experimental)
Please see [FEATURES.md](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/FEATURES.md)
## Notes about the Pebble Time
## Getting Started (Pebble)
All features are also supported on the Pebble Time, except for the following:
1. Pair your Pebble through the Android's Bluetooth Settings or Gadgetbridge. Pebble 2 MUST be paired though Gadgetbridge (tap on the + in Control Center)
* Listing installed watchfaces (it will simply display the UUIDs of previously installed watchapps, no matter if they are still installed or not)
* Firmware installation is untested and will probably not work.
## How to use (Pebble)
1. Pair your Pebble through Gadgetbridge's Discovery Activity or the Android Bluetooth Settings
2. Start Gadgetbridge, tap on the device you want to connect to
3. To test, choose "Debug" from the menu and play around
For more information read [this wiki article](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
## Features (Mi Band)
## How to use (Mi Band 1+2)
* Mi Band notifications (LEDs + vibration) for
* Discovery and pairing
* Incoming calls
* SMS received
* K-9 mails received
* Generic Android notifications
* Synchronize the time to the Mi Band
* Display firmware version and battery state
* Synchronize activity data
* Display sleep data (alpha)
* Display sports data (step count) (alpha)
* Set alarms on the Mi Band
* When starting Gadgetbridge the first time, it will automatically
attempt to discover and pair your Mi Band. Alternatively you can invoke discovery
manually via the "+" button. It will ask you for some personal info that appears
## How to use (Mi Band)
* When starting Gadgetbridge and no device is visible, it will automatically
attempt to discover and pair your Mi Band. Alternatively you can invoke this
manually via the menu button. It will ask you for some personal info that appears
to be needed for proper steps calculation on the band. If you do not provide these,
some hardcoded default "dummy" values will be used instead.
When your Mi Band starts to vibrate and blink during the pairing process,
When your Mi Band starts to vibrate and blink with all three LEDs during the pairing process,
tap it quickly a few times in a row to confirm the pairing with the band.
1. Configure other notifications as desired
2. Go back to the "Gadgetbridge" activity
3. Tap the Mi Band item to connect if you're not connected yet
2. Go back to the "Gadgetbridge" Activity
3. Tap the "MI" item to connect if you're not connected yet.
4. To test, chose "Debug" from the menu and play around
**Known Issues:**
Known Issues:
* Android 4.4+ only, we can only change this by not handling generic
notifications or by using AccessibiltyService. Don't know if it is worth the
hassle.
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
knows your MAC address. This behavior may also only occur with older firmware versions.
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
try to unpair the band in the other app and try again with Gadgetbridge.
* While all Mi Band devices are supported, some firmware versions might work better than others.
You can consult the [projects wiki pages](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
to check if your firmware version is fully supported or if an upgrade/downgrade might be beneficial.
## Features (Liveview)
* set time (automatically upon connection)
* display notifications and vibrate
## Authors
### Core Team (in order of first code contribution)
## Authors (in order of first code contribution)
* Andreas Shimokawa
* Carsten Pfeiffer
* Daniele Gobbetti
### Additional device support
* João Paulo Barraca (HPlus)
* Vitaly Svyastyn (NO.1 F1)
* Sami Alaoui (Teclast H30)
## Contribute
Contributions are welcome, be it feedback, bugreports, documentation, translation, research or code. Feel free to work
on any of the open [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues?q=is%3Aopen+is%3Aissue);
just leave a comment that you're working on one to avoid duplicated work.
Translations can be contributed via https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/
## Do you have further questions or feedback?
Feel free to open an issue on our issue tracker, but please:
- do not use the issue tracker as a forum, do not ask for ETAs and read the issue conversation before posting
- use the search functionality to ensure that your question wasn't already answered. Don't forget to check the **closed** issues as well!
- remember that this is a community project, people are contributing in their free time because they like doing so: don't take the fun away! Be kind and constructive.
Translations can be contributed via https://www.transifex.com/projects/p/gadgetbridge/resource/strings/ or
manually.
## Having problems?
0. Phone crashing during device discovery? Disable Privacy Guard (or similarly named functionality) during discovery.
1. Open Gadgetbridge's settings and check the option to write log files
2. Reproduce the problem you encountered
3. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
4. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
2. Quit Gadgetbridge and restart it
3. Reproduce the problem you encountered
4. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
5. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
Alternatively you may use the standard logcat functionality to access the log.

View File

@ -1,34 +1,19 @@
apply plugin: 'com.android.application'
apply plugin: 'findbugs'
apply plugin: 'pmd'
def ABORT_ON_CHECK_FAILURE=false
tasks.withType(Test) {
systemProperty 'MiFirmwareDir', System.getProperty('MiFirmwareDir', null)
systemProperty 'logback.configurationFile', System.getProperty('user.dir', null) + '/app/src/main/assets/logback.xml'
systemProperty 'GB_LOGFILES_DIR', java.nio.file.Files.createTempDirectory('gblog').toString();
}
android {
compileOptions {
// for KitKat
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
compileSdkVersion 25
buildToolsVersion '25.0.2'
compileSdkVersion 23
buildToolsVersion "23.0.0"
defaultConfig {
applicationId "nodomain.freeyourgadget.gadgetbridge"
minSdkVersion 19
targetSdkVersion 25
// note: always bump BOTH versionCode and versionName!
versionName "0.21.5"
versionCode 106
vectorDrawables.useSupportLibrary = true
targetSdkVersion 23
versionCode 25
versionName "0.5.4"
}
buildTypes {
release {
@ -52,42 +37,17 @@ android {
}
}
pmd {
toolVersion = '5.5.5'
}
dependencies {
// testCompile 'ch.qos.logback:logback-classic:1.1.3'
// 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.3.2"
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:cardview-v7:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:gridlayout-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:palette-v7:25.3.1'
compile 'com.github.tony19:logback-android-classic:1.1.1-6'
compile 'com.android.support:appcompat-v7:23.0.0'
compile 'com.android.support:support-v4:23.0.0'
compile 'com.github.tony19:logback-android-classic:1.1.1-3'
compile 'org.slf4j:slf4j-api:1.7.7'
compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'
compile 'com.github.PhilJay:MPAndroidChart:2.1.0'
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'
// 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")
}
preBuild.dependsOn(":GBDaoGenerator:genSources")
gradle.beforeProject {
preBuild.dependsOn(":GBDaoGenerator:genSources")
}
check.dependsOn 'findbugs', 'pmd', 'lint'
@ -133,6 +93,7 @@ task pmd(type: Pmd) {
}
}
task findbugs(type: FindBugs) {
ignoreFailures = !ABORT_ON_CHECK_FAILURE
effort = "default"

View File

@ -2,22 +2,21 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nodomain.freeyourgadget.gadgetbridge">
<!--
Comment in for testing Pebble Emulator
<uses-permission android:name="android.permission.INTERNET" />
-->
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="23" />
<!--
<uses-permission android:name="android.permission.INTERNET" />
-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.fsck.k9.permission.READ_MESSAGES" />
<uses-feature
android:name="android.hardware.bluetooth"
@ -25,55 +24,64 @@
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<application
android:name=".GBApplication"
android:allowBackup="false"
android:fullBackupContent="false"
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/GadgetbridgeTheme">
<activity
android:name=".activities.ControlCenterv2"
android:label="@string/title_activity_controlcenter"
android:theme="@style/GadgetbridgeTheme.NoActionBar">
android:name=".activities.ControlCenter"
android:label="@string/title_activity_controlcenter">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".activities.ControlCenterv2" />
android:label="@string/title_activity_settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ControlCenter" />
</activity>
<activity
android:name=".devices.miband.MiBandPreferencesActivity"
android:label="@string/preferences_miband_settings"
android:parentActivityName=".activities.SettingsActivity" />
android:label="@string/preferences_miband_settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.SettingsActivity" />
</activity>
<activity
android:launchMode="singleTop"
android:name=".activities.appmanager.AppManagerActivity"
android:label="@string/title_activity_appmanager"
android:parentActivityName=".activities.ControlCenterv2" />
android:name=".activities.AppManagerActivity"
android:label="@string/title_activity_appmanager">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ControlCenter" />
</activity>
<activity
android:name=".activities.AppBlacklistActivity"
android:label="@string/title_activity_appblacklist"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.CalBlacklistActivity"
android:label="@string/title_activity_calblacklist"
android:parentActivityName=".activities.SettingsActivity" />
android:label="@string/title_activity_appblacklist">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.SettingsActivity" />
</activity>
<activity
android:name=".activities.FwAppInstallerActivity"
android:label="@string/title_activity_fw_app_insaller"
android:parentActivityName=".activities.ControlCenterv2">
android:label="@string/title_activity_fw_app_insaller">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ControlCenter" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
@ -94,46 +102,7 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\.ft" />
<data android:pathPattern="/.*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\.res" />
<data android:pathPattern="/.*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\.gps" />
<data android:pathPattern="/.*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
@ -144,6 +113,7 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\.pbz" />
<data android:pathPattern="/.*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
@ -154,20 +124,10 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\.pbl" />
<data android:pathPattern="/.*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<!-- no mimeType filter, needed for CM-derived ROMs? -->
@ -187,46 +147,7 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
<data android:pathPattern="/.*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
<data android:pathPattern="/.*\\.ft" />
<data android:pathPattern="/.*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\.res" />
<data android:pathPattern="/.*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\.gps" />
<data android:pathPattern="/.*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
@ -237,6 +158,7 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
<data android:pathPattern="/.*\\.pbz" />
<data android:pathPattern="/.*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
@ -247,42 +169,6 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
<data android:pathPattern="/.*\\.pbl" />
<data android:pathPattern="/.*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
</intent-filter>
<!-- to receive the firmwares from the download content provider -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<!-- to receive firmwares from the download content provider if recognized as zip-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/x-zip-compressed" />
</intent-filter>
<!-- to receive files from the "share" intent -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
@ -295,31 +181,57 @@
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service android:name=".service.NotificationCollectorMonitorService" />
<service android:name=".service.DeviceCommunicationService" />
<receiver
android:name=".externalevents.WeatherNotificationReceiver"
android:enabled="true">
android:name=".externalevents.PhoneCallReceiver"
android:enabled="false">
<intent-filter>
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_UPDATE_2" />
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
<activity android:name=".externalevents.WeatherNotificationConfig">
<receiver
android:name=".externalevents.SMSReceiver"
android:enabled="false">
<intent-filter>
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_SKIN_PREFERENCES"/>
</intent-filter>
</activity>
<receiver android:name=".externalevents.AutoStartReceiver"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.TimeChangeReceiver"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
<action android:name="android.intent.action.TIME_SET" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.K9Receiver"
android:enabled="false">
<intent-filter>
<data android:scheme="email" />
<action android:name="com.fsck.k9.intent.action.EMAIL_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.PebbleReceiver"
android:enabled="false">
<intent-filter>
<action android:name="com.getpebble.action.SEND_NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.MusicPlaybackReceiver"
android:enabled="false">
<intent-filter>
<action android:name="com.andrew.apollo.metachanged" />
<action android:name="com.cyanogenmod.eleven.metachanged" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.BluetoothStateChangeReceiver"
android:exported="false">
@ -342,100 +254,50 @@
</intent-filter>
</receiver>
<!--
forcing the DebugActivity to portrait mode avoids crashes with the progress
dialog when changing orientation
-->
<activity
android:name=".activities.DebugActivity"
android:label="@string/title_activity_debug"
android:parentActivityName=".activities.ControlCenterv2"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".activities.DbManagementActivity"
android:label="@string/title_activity_db_management"
android:parentActivityName=".activities.ControlCenterv2"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
android:label="@string/title_activity_debug">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ControlCenter" />
</activity>
<activity
android:name=".activities.DiscoveryActivity"
android:label="@string/title_activity_discovery"
android:parentActivityName=".activities.ControlCenterv2" />
android:label="@string/title_activity_discovery">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ControlCenter" />
</activity>
<activity
android:name=".activities.AndroidPairingActivity"
android:label="@string/title_activity_android_pairing" />
<activity
android:name=".devices.miband.MiBandPairingActivity"
android:label="@string/title_activity_mi_band_pairing" />
<activity
android:name=".devices.pebble.PebblePairingActivity"
android:label="@string/title_activity_pebble_pairing" />
<activity
android:name=".activities.charts.ChartsActivity"
android:label="@string/title_activity_charts"
android:parentActivityName=".activities.ControlCenterv2" />
android:parentActivityName=".activities.ControlCenter">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ControlCenter" />
</activity>
<activity
android:name=".activities.ConfigureAlarms"
android:label="@string/title_activity_set_alarm"
android:parentActivityName=".activities.SettingsActivity" />
android:parentActivityName=".activities.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.SettingsActivity" />
</activity>
<activity
android:name=".activities.AlarmDetails"
android:label="@string/title_activity_alarm_details"
android:screenOrientation="portrait"
android:parentActivityName=".activities.ConfigureAlarms" />
<activity
android:name=".activities.VibrationActivity"
android:label="@string/title_activity_vibration"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.AudioSettingsActivity"
android:label="@string/title_audio_activity"
android:parentActivityName=".activities.ControlCenterv2" />
<provider
android:name=".contentprovider.PebbleContentProvider"
android:authorities="com.getpebble.android.provider"
android:exported="true" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.screenshot_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/screenshot_provider_paths"/>
</provider>
<receiver android:name=".SleepAlarmWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/sleep_alarm_widget_info" />
</receiver>
<activity
android:launchMode="singleTask"
android:allowTaskReparenting="true"
android:clearTaskOnLaunch="true"
android:name=".activities.ExternalPebbleJSActivity"
android:label="@string/app_configure"
android:parentActivityName=".activities.appmanager.AppManagerActivity">
android:parentActivityName=".activities.ConfigureAlarms">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2" />
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="gadgetbridge" />
</intent-filter>
android:value="nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms" />
</activity>
</application>
</manifest>

View File

@ -1,102 +0,0 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<meta name='viewport' content='initial-scale=1.0, maximum-scale=1.0'>
<script type="text/javascript" src="js/Uri.js">
</script>
<script type="text/javascript" src="js/gadgetbridge_boilerplate.js">
</script>
<script type="text/javascript">
</script>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
iframe {
display: block;
width: 100%;
height: 100%;
border: none;
}
body {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-touch-callout: none;
}
#config_url,#jsondata {
word-wrap: break-word;
margin: 20px 0;
width: 90%;
}
.btn {
display: inline-block;
position: relative;
height: 32px;
line-height: 32px;
border-radius: 2px;
font-size: 0.9em;
background-color: #eee;
color: #646464;
text-align: center;
border-style: none;
}
.btn:active {
border-style: none;
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2)inset;
transition-delay: 0s;
}
p {
width: 90%;
}
#pastereturn {
width: 90%;
min-height: 3em;
}
#step1compat, #step2 {
display: none;
}
<!-- TODO -->
</style>
</head>
<body>
<div id="step1" class="step">
<h2>URL of the configuration:</h2>
<div id="config_url"></div>
<!--<button class="btn" name="show config" value="show config" onclick="Pebble.showConfiguration()" >Show config / URL</button>-->
<button class="btn" name="open config" value="open config" onclick="Pebble.actuallyOpenURL()">
Open configuration website
</button>
<h2 class="load_presets">App presets:</h2>
<button class="btn load_presets" name="read config" value="read config"
onclick="Pebble.loadPreset()">
Load saved configuration
</button>
</div>
<div id="step1compat" class="step">
<p>In case of "network error" after saving settings in the watchapp, copy the "network error"
URL and paste it here:</p>
<textarea id="pastereturn"></textarea><br/>
<button class="btn" name="parse" onclick="Pebble.parseReturnedPebbleJS()">Parse legacy app
configuration
</button>
</div>
<div id="step2" class="step">
<h2>Incoming configuration data:</h2>
<div id="jsondata"></div>
<button class="btn" name="send config" value="send config" onclick="Pebble.actuallySendData()">
Send data to pebble
</button>
<h2 class="store_presets">App Presets:</h2>
<button class="btn store_presets" name="store config" value="store config"
onclick="Pebble.savePreset()">
Store incoming configuration
</button>
<p class="store_presets">Existing presets will be deleted.</p>
</div>
</body>

View File

@ -1,458 +0,0 @@
/*!
* jsUri
* https://github.com/derek-watson/jsUri
*
* Copyright 2013, Derek Watson
* Released under the MIT license.
*
* Includes parseUri regular expressions
* http://blog.stevenlevithan.com/archives/parseuri
* Copyright 2007, Steven Levithan
* Released under the MIT license.
*/
/*globals define, module */
(function(global) {
var re = {
starts_with_slashes: /^\/+/,
ends_with_slashes: /\/+$/,
pluses: /\+/g,
query_separator: /[&;]/,
uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@\/]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
};
/**
* Define forEach for older js environments
* @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach#Compatibility
*/
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
if (this == null) {
throw new TypeError(' this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = thisArg;
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
callback.call(T, kValue, k, O);
}
k++;
}
};
}
/**
* unescape a query param value
* @param {string} s encoded value
* @return {string} decoded value
*/
function decode(s) {
if (s) {
s = s.toString().replace(re.pluses, '%20');
s = decodeURIComponent(s);
}
return s;
}
/**
* Breaks a uri string down into its individual parts
* @param {string} str uri
* @return {object} parts
*/
function parseUri(str) {
var parser = re.uri_parser;
var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"];
var m = parser.exec(str || '');
var parts = {};
parserKeys.forEach(function(key, i) {
parts[key] = m[i] || '';
});
return parts;
}
/**
* Breaks a query string down into an array of key/value pairs
* @param {string} str query
* @return {array} array of arrays (key/value pairs)
*/
function parseQuery(str) {
var i, ps, p, n, k, v, l;
var pairs = [];
if (typeof(str) === 'undefined' || str === null || str === '') {
return pairs;
}
if (str.indexOf('?') === 0) {
str = str.substring(1);
}
ps = str.toString().split(re.query_separator);
for (i = 0, l = ps.length; i < l; i++) {
p = ps[i];
n = p.indexOf('=');
if (n !== 0) {
k = decode(p.substring(0, n));
v = decode(p.substring(n + 1));
pairs.push(n === -1 ? [p, null] : [k, v]);
}
}
return pairs;
}
/**
* Creates a new Uri object
* @constructor
* @param {string} str
*/
function Uri(str) {
this.uriParts = parseUri(str);
this.queryPairs = parseQuery(this.uriParts.query);
this.hasAuthorityPrefixUserPref = null;
}
/**
* Define getter/setter methods
*/
['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function(key) {
Uri.prototype[key] = function(val) {
if (typeof val !== 'undefined') {
this.uriParts[key] = val;
}
return this.uriParts[key];
};
});
/**
* if there is no protocol, the leading // can be enabled or disabled
* @param {Boolean} val
* @return {Boolean}
*/
Uri.prototype.hasAuthorityPrefix = function(val) {
if (typeof val !== 'undefined') {
this.hasAuthorityPrefixUserPref = val;
}
if (this.hasAuthorityPrefixUserPref === null) {
return (this.uriParts.source.indexOf('//') !== -1);
} else {
return this.hasAuthorityPrefixUserPref;
}
};
Uri.prototype.isColonUri = function (val) {
if (typeof val !== 'undefined') {
this.uriParts.isColonUri = !!val;
} else {
return !!this.uriParts.isColonUri;
}
};
/**
* Serializes the internal state of the query pairs
* @param {string} [val] set a new query string
* @return {string} query string
*/
Uri.prototype.query = function(val) {
var s = '', i, param, l;
if (typeof val !== 'undefined') {
this.queryPairs = parseQuery(val);
}
for (i = 0, l = this.queryPairs.length; i < l; i++) {
param = this.queryPairs[i];
if (s.length > 0) {
s += '&';
}
if (param[1] === null) {
s += param[0];
} else {
s += param[0];
s += '=';
if (typeof param[1] !== 'undefined') {
s += encodeURIComponent(param[1]);
}
}
}
return s.length > 0 ? '?' + s : s;
};
/**
* returns the first query param value found for the key
* @param {string} key query key
* @return {string} first value found for key
*/
Uri.prototype.getQueryParamValue = function (key) {
var param, i, l;
for (i = 0, l = this.queryPairs.length; i < l; i++) {
param = this.queryPairs[i];
if (key === param[0]) {
return param[1];
}
}
};
/**
* returns an array of query param values for the key
* @param {string} key query key
* @return {array} array of values
*/
Uri.prototype.getQueryParamValues = function (key) {
var arr = [], i, param, l;
for (i = 0, l = this.queryPairs.length; i < l; i++) {
param = this.queryPairs[i];
if (key === param[0]) {
arr.push(param[1]);
}
}
return arr;
};
/**
* removes query parameters
* @param {string} key remove values for key
* @param {val} [val] remove a specific value, otherwise removes all
* @return {Uri} returns self for fluent chaining
*/
Uri.prototype.deleteQueryParam = function (key, val) {
var arr = [], i, param, keyMatchesFilter, valMatchesFilter, l;
for (i = 0, l = this.queryPairs.length; i < l; i++) {
param = this.queryPairs[i];
keyMatchesFilter = decode(param[0]) === decode(key);
valMatchesFilter = param[1] === val;
if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
arr.push(param);
}
}
this.queryPairs = arr;
return this;
};
/**
* adds a query parameter
* @param {string} key add values for key
* @param {string} val value to add
* @param {integer} [index] specific index to add the value at
* @return {Uri} returns self for fluent chaining
*/
Uri.prototype.addQueryParam = function (key, val, index) {
if (arguments.length === 3 && index !== -1) {
index = Math.min(index, this.queryPairs.length);
this.queryPairs.splice(index, 0, [key, val]);
} else if (arguments.length > 0) {
this.queryPairs.push([key, val]);
}
return this;
};
/**
* test for the existence of a query parameter
* @param {string} key check values for key
* @return {Boolean} true if key exists, otherwise false
*/
Uri.prototype.hasQueryParam = function (key) {
var i, len = this.queryPairs.length;
for (i = 0; i < len; i++) {
if (this.queryPairs[i][0] == key)
return true;
}
return false;
};
/**
* replaces query param values
* @param {string} key key to replace value for
* @param {string} newVal new value
* @param {string} [oldVal] replace only one specific value (otherwise replaces all)
* @return {Uri} returns self for fluent chaining
*/
Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) {
var index = -1, len = this.queryPairs.length, i, param;
if (arguments.length === 3) {
for (i = 0; i < len; i++) {
param = this.queryPairs[i];
if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) {
index = i;
break;
}
}
if (index >= 0) {
this.deleteQueryParam(key, decode(oldVal)).addQueryParam(key, newVal, index);
}
} else {
for (i = 0; i < len; i++) {
param = this.queryPairs[i];
if (decode(param[0]) === decode(key)) {
index = i;
break;
}
}
this.deleteQueryParam(key);
this.addQueryParam(key, newVal, index);
}
return this;
};
/**
* Define fluent setter methods (setProtocol, setHasAuthorityPrefix, etc)
*/
['protocol', 'hasAuthorityPrefix', 'isColonUri', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function(key) {
var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
Uri.prototype[method] = function(val) {
this[key](val);
return this;
};
});
/**
* Scheme name, colon and doubleslash, as required
* @return {string} http:// or possibly just //
*/
Uri.prototype.scheme = function() {
var s = '';
if (this.protocol()) {
s += this.protocol();
if (this.protocol().indexOf(':') !== this.protocol().length - 1) {
s += ':';
}
s += '//';
} else {
if (this.hasAuthorityPrefix() && this.host()) {
s += '//';
}
}
return s;
};
/**
* Same as Mozilla nsIURI.prePath
* @return {string} scheme://user:password@host:port
* @see https://developer.mozilla.org/en/nsIURI
*/
Uri.prototype.origin = function() {
var s = this.scheme();
if (this.userInfo() && this.host()) {
s += this.userInfo();
if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) {
s += '@';
}
}
if (this.host()) {
s += this.host();
if (this.port() || (this.path() && this.path().substr(0, 1).match(/[0-9]/))) {
s += ':' + this.port();
}
}
return s;
};
/**
* Adds a trailing slash to the path
*/
Uri.prototype.addTrailingSlash = function() {
var path = this.path() || '';
if (path.substr(-1) !== '/') {
this.path(path + '/');
}
return this;
};
/**
* Serializes the internal state of the Uri object
* @return {string}
*/
Uri.prototype.toString = function() {
var path, s = this.origin();
if (this.isColonUri()) {
if (this.path()) {
s += ':'+this.path();
}
} else if (this.path()) {
path = this.path();
if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
s += '/';
} else {
if (s) {
s.replace(re.ends_with_slashes, '/');
}
path = path.replace(re.starts_with_slashes, '/');
}
s += path;
} else {
if (this.host() && (this.query().toString() || this.anchor())) {
s += '/';
}
}
if (this.query().toString()) {
s += this.query().toString();
}
if (this.anchor()) {
if (this.anchor().indexOf('#') !== 0) {
s += '#';
}
s += this.anchor();
}
return s;
};
/**
* Clone a Uri object
* @return {Uri} duplicate copy of the Uri
*/
Uri.prototype.clone = function() {
return new Uri(this.toString());
};
/**
* export via AMD or CommonJS, otherwise leak a global
*/
if (typeof define === 'function' && define.amd) {
define(function() {
return Uri;
});
} else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = Uri;
} else {
global.Uri = Uri;
}
}(this));

View File

@ -1,254 +0,0 @@
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){
var prefix = GBjs.getAppLocalstoragePrefix();
GBjs.gbLog("redefining local storage with prefix: " + prefix);
Storage.prototype.setItem = (function(key, value) {
this.call(localStorage,prefix + key, value);
}).bind(Storage.prototype.setItem);
Storage.prototype.getItem = (function(key) {
// console.log("I am about to return " + prefix + key);
var def = null;
if(key == 'clay-settings') {
def = '{}';
}
return this.call(localStorage,prefix + key) || def;
}).bind(Storage.prototype.getItem);
}
function loadScript(url, callback) {
// Adding the script tag to the head as suggested before
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// Then bind the event to the callback function.
// There are several events for cross browser compatibility.
script.onreadystatechange = callback;
script.onload = callback;
// Fire the loading
head.appendChild(script);
}
function getURLVariable(variable, defaultValue) {
// Find all URL parameters
var query = location.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
// If the query variable parameter is found, decode it to use and return it for use
if (pair[0] === variable) {
return decodeURIComponent(pair[1]);
}
}
return defaultValue || false;
}
function showStep(desiredStep) {
var steps = document.getElementsByClassName("step");
var testStep = null;
for (var i = 0; i < steps.length; i ++) {
if (steps[i].id == desiredStep)
testStep = steps[i].id;
}
if (testStep !== null) {
for (var i = 0; i < steps.length; i ++) {
steps[i].style.display = 'none';
}
document.getElementById(desiredStep).style.display="block";
}
}
function hideSteps() {
var steps = document.getElementsByClassName("step");
for (var i = 0; i < steps.length; i ++) {
steps[i].style.display = 'none';
}
}
function gbPebble() {
this.configurationURL = null;
this.configurationValues = null;
var self = this;
self.events = {};
//events processing: see http://stackoverflow.com/questions/10978311/implementing-events-in-my-own-object
self.addEventListener = function(name, handler) {
if (self.events.hasOwnProperty(name))
self.events[name].push(handler);
else
self.events[name] = [handler];
}
self.removeEventListener = function(name, handler) {
if (!self.events.hasOwnProperty(name))
return;
var index = self.events[name].indexOf(handler);
if (index != -1)
self.events[name].splice(index, 1);
}
self.evaluate = function(name, args) {
if (!self.events.hasOwnProperty(name))
return;
if (!args || !args.length)
args = [];
var evs = self.events[name], l = evs.length;
for (var i = 0; i < l; i++) {
evs[i].apply(null, args);
}
}
this.actuallyOpenURL = function() {
showStep("step1compat");
window.open(self.configurationURL.toString(), "config");
}
this.actuallySendData = function() {
GBjs.sendAppMessage(self.configurationValues);
GBjs.closeActivity();
}
this.savePreset = function() {
GBjs.saveAppStoredPreset(self.configurationValues);
}
this.loadPreset = function() {
showStep("step2");
var presetElements = document.getElementsByClassName("store_presets");
for (var i = 0; i < presetElements.length; i ++) {
presetElements[i].style.display = 'none';
}
self.configurationValues = GBjs.getAppStoredPreset();
document.getElementById("jsondata").innerHTML=self.configurationValues;
}
//needs to be called like this because of original Pebble function name
this.openURL = function(url) {
if (url.lastIndexOf("http", 0) === 0) {
document.getElementById("config_url").innerHTML=url;
var UUID = GBjs.getAppUUID();
self.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
} else {
//TODO: add custom return_to
var iframe = document.getElementsByTagName('iframe')[0];
var oldbody = document.getElementsByTagName("body")[0];
if (iframe === undefined && oldbody !== undefined) {
iframe = document.createElement("iframe");
oldbody.parentNode.replaceChild(iframe,oldbody);
} else {
hideSteps();
document.documentElement.appendChild(iframe);
}
iframe.src = url;
}
}
this.getActiveWatchInfo = function() {
return JSON.parse(GBjs.getActiveWatchInfo());
}
this.sendAppMessage = function (dict, callbackAck, callbackNack){
try {
self.configurationValues = JSON.stringify(dict);
document.getElementById("jsondata").innerHTML=self.configurationValues;
if (callbackAck != undefined) {
callbackAck();
}
}
catch (e) {
GBjs.gbLog("sendAppMessage failed");
if (callbackNack != undefined) {
callbackNack();
}
}
}
this.getAccountToken = function() {
return '';
}
this.getWatchToken = function() {
return GBjs.getWatchToken();
}
this.getTimelineToken = function() {
return '';
}
this.showSimpleNotificationOnPebble = function(title, body) {
GBjs.gbLog("app wanted to show: " + title + " body: "+ body);
}
this.showConfiguration = function() {
console.error("This watchapp doesn't support configuration");
GBjs.closeActivity();
}
this.parseReturnedPebbleJS = function() {
var str = document.getElementById('pastereturn').value;
var needle = "pebblejs://close#";
if (str.split(needle)[1] !== undefined) {
var t = new Object();
t.response = decodeURIComponent(str.split(needle)[1]);
self.evaluate('webviewclosed',[t]);
showStep("step2");
} else {
console.error("No valid configuration found in the entered string.");
}
}
}
var Pebble = new gbPebble();
var jsConfigFile = GBjs.getAppConfigurationFile();
var storedPreset = GBjs.getAppStoredPreset();
document.addEventListener('DOMContentLoaded', function(){
if (jsConfigFile != null) {
loadScript(jsConfigFile, function() {
Pebble.evaluate('ready', [{'type': "ready"}]); //callback object apparently needed by some watchfaces
if (getURLVariable('config') == 'true') {
showStep("step2");
var json_string = getURLVariable('json');
var t = new Object();
t.response = json_string;
if (json_string != '') {
Pebble.evaluate('webviewclosed',[t]);
}
} else {
if (storedPreset === undefined) {
var presetElements = document.getElementsByClassName("load_presets");
for (var i = 0; i < presetElements.length; i ++) {
presetElements[i].style.display = 'none';
}
}
Pebble.evaluate('showConfiguration');
}
});
}
}, false);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,175 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<XMI timestamp="2016-01-26T23:02:14" verified="false" xmi.version="1.2" xmlns:UML="http://schema.omg.org/spec/UML/1.3">
<XMI.header>
<XMI.documentation>
<XMI.exporter>umbrello uml modeller http://umbrello.kde.org</XMI.exporter>
<XMI.exporterVersion>1.6.9</XMI.exporterVersion>
<XMI.exporterEncoding>UnicodeUTF8</XMI.exporterEncoding>
</XMI.documentation>
<XMI.metamodel xmi.name="UML" href="UML.xml" xmi.version="1.3"/>
</XMI.header>
<XMI.content>
<UML:Model isAbstract="false" isRoot="false" isSpecification="false" name="UML Model" isLeaf="false" xmi.id="m1">
<UML:Namespace.ownedElement>
<UML:Stereotype isRoot="false" isAbstract="false" isSpecification="false" name="folder" isLeaf="false" namespace="m1" visibility="public" xmi.id="folder"/>
<UML:Stereotype isRoot="false" isAbstract="false" isSpecification="false" name="datatype" isLeaf="false" namespace="m1" visibility="public" xmi.id="datatype"/>
<UML:Stereotype isRoot="false" isAbstract="false" isSpecification="false" name="interface" isLeaf="false" namespace="m1" visibility="public" xmi.id="interface"/>
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Logical View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Logical View">
<UML:Namespace.ownedElement>
<UML:Package isRoot="false" isAbstract="false" isSpecification="false" name="Datatypes" isLeaf="false" stereotype="folder" namespace="Logical View" visibility="public" xmi.id="Datatypes">
<UML:Namespace.ownedElement>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="int" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="tM1MfT3dGbDn"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="char" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="MZCr2zI6yZo6"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="bool" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="mtFc0pEuADEp"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="float" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="hIRvrcjDBt2B"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="double" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="tIHrr3vdvBFv"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="short" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="yRn8f2wKINF9"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="long" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="xwL6qLHheXWT"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="unsigned int" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="aS7VdKRbkCiP"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="unsigned short" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="EejU1wcwrx2h"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="unsigned long" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="GJMsqsMRIRuv"/>
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="string" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="jLVTWBskpZFo"/>
</UML:Namespace.ownedElement>
</UML:Package>
<UML:Interface isRoot="false" isAbstract="true" isSpecification="false" name="DeviceService" isLeaf="false" stereotype="interface" namespace="Logical View" visibility="public" xmi.id="VA6qSCiBtNc5"/>
<UML:Interface isRoot="false" isAbstract="true" isSpecification="false" name="DeviceSupport" isLeaf="false" stereotype="interface" namespace="Logical View" visibility="public" xmi.id="AKxHpDCgOPhk"/>
</UML:Namespace.ownedElement>
<XMI.extension xmi.extender="umbrello">
<diagrams>
<diagram showgrid="0" snapgrid="0" backgroundcolor="#ffffff" zoom="100" type="1" linecolor="#ff0000" usefillcolor="1" griddotcolor="#d3d3d3" showops="1" showscope="1" localid="-1" showpackage="1" showpubliconly="0" canvaswidth="0" isopen="0" showatts="1" xmi.id="31Hk7F5Bq9ac" snapcsgrid="0" snapx="25" name="class diagram" showattsig="1" textcolor="#000000" showstereotype="1" showattribassocs="1" linewidth="0" fillcolor="#ffff00" documentation="" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" canvasheight="0" showopsig="1" snapy="25">
<widgets/>
<messages/>
<associations/>
</diagram>
</diagrams>
</XMI.extension>
</UML:Model>
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Use Case View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Use Case View">
<UML:Namespace.ownedElement/>
</UML:Model>
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Component View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Component View">
<UML:Namespace.ownedElement>
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="Activities" executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="CDLTeFMNZrp5"/>
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="DeviceCommunicationService" executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="72YBMP5WcQzE">
<UML:Namespace.ownedElement>
<UML:Port isRoot="false" isAbstract="false" isSpecification="false" name="pin" isLeaf="false" namespace="72YBMP5WcQzE" visibility="public" xmi.id="xhXzgXGeAGP8"/>
<UML:Port isRoot="false" isAbstract="false" isSpecification="false" name="pin2" isLeaf="false" namespace="72YBMP5WcQzE" visibility="public" xmi.id="LcpVBJq9yM8d"/>
</UML:Namespace.ownedElement>
</UML:Component>
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="Frontend2" executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="zd0ny2K62qHM"/>
<UML:Artifact isRoot="false" isAbstract="false" drawas="0" isSpecification="false" name="Service" isLeaf="false" namespace="Component View" visibility="public" xmi.id="jjfTCGwVsjy4"/>
<UML:Abstraction supplier="VA6qSCiBtNc5" isSpecification="false" name="" namespace="Component View" client="72YBMP5WcQzE" visibility="public" xmi.id="3ZwKCCl82Cel"/>
<UML:Association isSpecification="false" name="invoke" namespace="Component View" visibility="public" xmi.id="ZmBbwqKcFFuU">
<UML:Association.connection>
<UML:AssociationEnd isNavigable="false" changeability="changeable" isSpecification="false" type="CDLTeFMNZrp5" name="" aggregation="none" visibility="public" xmi.id="8FlknwQC5gyi"/>
<UML:AssociationEnd isNavigable="true" changeability="changeable" isSpecification="false" type="VA6qSCiBtNc5" name="" aggregation="none" visibility="public" xmi.id="0RPtmPzL9Au6"/>
</UML:Association.connection>
</UML:Association>
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="Concrete Device Impl." executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="tDF2L96KKnVj"/>
<UML:Association isSpecification="false" name="invoke" namespace="Component View" visibility="public" xmi.id="60vlquxGX0xa">
<UML:Association.connection>
<UML:AssociationEnd isNavigable="false" changeability="changeable" isSpecification="false" type="72YBMP5WcQzE" name="" aggregation="none" visibility="public" xmi.id="EElpRDBsrFht"/>
<UML:AssociationEnd isNavigable="true" changeability="changeable" isSpecification="false" type="AKxHpDCgOPhk" name="" aggregation="none" visibility="public" xmi.id="zEQUpqXtpTfd"/>
</UML:Association.connection>
</UML:Association>
<UML:Abstraction supplier="AKxHpDCgOPhk" isSpecification="false" name="" namespace="Component View" client="tDF2L96KKnVj" visibility="public" xmi.id="LcusapQo1BbT"/>
</UML:Namespace.ownedElement>
<XMI.extension xmi.extender="umbrello">
<diagrams>
<diagram showgrid="0" snapgrid="0" backgroundcolor="#ffffff" zoom="105" type="7" linecolor="#ff0000" usefillcolor="1" griddotcolor="#d3d3d3" showops="1" showscope="1" localid="-1" showpackage="1" showpubliconly="0" canvaswidth="1931" isopen="1" showatts="1" xmi.id="UpkLkm005Mtw" snapcsgrid="0" snapx="25" name="Gadgetbridge" showattsig="1" textcolor="#000000" showstereotype="1" showattribassocs="1" linewidth="0" fillcolor="#ffff00" documentation="" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" canvasheight="1133" showopsig="1" snapy="25">
<widgets>
<componentwidget textcolor="#000000" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" height="102" linewidth="0" width="165" showstereotype="1" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="0" x="-1652,09756097561" y="-881,8487804878049" localid="2hnpF0djpL2Z" usesdiagramfillcolor="0" xmi.id="CDLTeFMNZrp5" isinstance="0"/>
<componentwidget textcolor="#000000" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" height="60" linewidth="0" width="246" showstereotype="1" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="0" x="-1654" y="-645" localid="dMZABODC4z1H" usesdiagramfillcolor="0" xmi.id="72YBMP5WcQzE" isinstance="0"/>
<interfacewidget linecolor="none" usefillcolor="1" usesdiagramfillcolor="0" x="-1589,839897589076" isinstance="0" width="40" localid="bZYTIWVHJeGR" showscope="1" height="40" drawascircle="1" showpubliconly="0" showpackage="0" xmi.id="VA6qSCiBtNc5" showattributes="0" textcolor="#000000" showstereotype="1" linewidth="0" usesdiagramusefillcolor="0" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" showopsigs="601" showattsigs="601" showoperations="1" y="-716,9293791337742">
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-91,07317073170748" isinstance="0" width="88" localid="wsTYjTmCdq05" height="19" posttext="" xmi.id="3iFu3ryeOW6f" role="700" textcolor="none" showstereotype="1" linewidth="none" text="DeviceService" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="10,97560975609758"/>
</interfacewidget>
<componentwidget textcolor="#000000" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" height="60" linewidth="0" width="195" showstereotype="1" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="0" x="-1374,768315059297" y="-548,2509733721189" localid="L9H7YahrGQFd" usesdiagramfillcolor="0" xmi.id="tDF2L96KKnVj" isinstance="0"/>
<interfacewidget linecolor="none" usefillcolor="1" usesdiagramfillcolor="0" x="-1320,4" isinstance="0" width="40" localid="Xn4rYXasfszD" showscope="1" height="40" drawascircle="1" showpubliconly="0" showpackage="0" xmi.id="AKxHpDCgOPhk" showattributes="0" textcolor="#000000" showstereotype="1" linewidth="0" usesdiagramusefillcolor="0" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" showopsigs="601" showattsigs="601" showoperations="1" y="-640,1853658536585">
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-6,602439024390151" isinstance="0" width="91" localid="Qe3g5GJDaFUO" height="19" posttext="" xmi.id="Qe4p2lZZXbQ3" role="700" textcolor="none" showstereotype="1" linewidth="none" text="DeviceSupport" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="-15,0634146341464"/>
</interfacewidget>
</widgets>
<messages/>
<associations>
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="72YBMP5WcQzE" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="94" type="511" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="VA6qSCiBtNc5" xmi.id="3ZwKCCl82Cel" indexa="1">
<linepath layout="Polyline">
<startpoint startx="-1569,718796671645" starty="-645"/>
<endpoint endx="-1569,718796671645" endy="-676,9293791337742"/>
</linepath>
</assocwidget>
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="CDLTeFMNZrp5" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="96" type="512" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="VA6qSCiBtNc5" xmi.id="ZmBbwqKcFFuU" indexa="1">
<linepath layout="Polyline">
<startpoint startx="-1571,551409184105" starty="-779,8487804878049"/>
<endpoint endx="-1571,551409184105" endy="-716,9293791337742"/>
</linepath>
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-1567,112384793861" isinstance="0" width="44" localid="yCgBPagYnPIa" height="19" posttext="" xmi.id="LilOMj6M9VGv" role="703" textcolor="none" showstereotype="1" linewidth="none" text="invoke" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="-758,1110310303017"/>
</assocwidget>
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="72YBMP5WcQzE" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="2" type="512" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="AKxHpDCgOPhk" xmi.id="60vlquxGX0xa" indexa="1">
<linepath layout="Polyline">
<startpoint startx="-1408" starty="-617,0146341463413"/>
<endpoint endx="-1320,4" endy="-617,0146341463413"/>
</linepath>
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-1382,614634146342" isinstance="0" width="44" localid="S2UTD5uasDos" height="19" posttext="" xmi.id="BrpNNGMiEFrg" role="703" textcolor="none" showstereotype="1" linewidth="none" text="invoke" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="-613,3439024390243"/>
</assocwidget>
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="tDF2L96KKnVj" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="53" type="511" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="AKxHpDCgOPhk" xmi.id="LcusapQo1BbT" indexa="1">
<linepath layout="Polyline">
<startpoint startx="-1299,912195121952" starty="-548,2509733721189"/>
<endpoint endx="-1299,912195121952" endy="-600,1853658536585"/>
</linepath>
</assocwidget>
</associations>
</diagram>
</diagrams>
</XMI.extension>
</UML:Model>
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Deployment View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Deployment View">
<UML:Namespace.ownedElement/>
</UML:Model>
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Entity Relationship Model" isLeaf="false" namespace="m1" visibility="public" xmi.id="Entity Relationship Model">
<UML:Namespace.ownedElement/>
</UML:Model>
</UML:Namespace.ownedElement>
</UML:Model>
</XMI.content>
<XMI.extensions xmi.extender="umbrello">
<docsettings viewid="UpkLkm005Mtw" uniqueid="BrpNNGMiEFrg" documentation=""/>
<listview>
<listitem type="800" id="Views" open="1">
<listitem type="821" id="Component View" open="1">
<listitem type="822" id="CDLTeFMNZrp5" open="1"/>
<listitem type="822" id="tDF2L96KKnVj" open="1"/>
<listitem type="822" id="72YBMP5WcQzE" open="1">
<listitem type="845" id="xhXzgXGeAGP8" open="1"/>
<listitem type="845" id="LcpVBJq9yM8d" open="1"/>
</listitem>
<listitem type="822" id="zd0ny2K62qHM" open="1"/>
<listitem type="819" label="Gadgetbridge" id="UpkLkm005Mtw" open="0"/>
<listitem type="824" id="jjfTCGwVsjy4" open="1"/>
</listitem>
<listitem type="827" id="Deployment View" open="1"/>
<listitem type="836" id="Entity Relationship Model" open="1"/>
<listitem type="801" id="Logical View" open="1">
<listitem type="807" label="class diagram" id="31Hk7F5Bq9ac" open="0"/>
<listitem type="830" id="Datatypes" open="0">
<listitem type="829" id="mtFc0pEuADEp" open="1"/>
<listitem type="829" id="MZCr2zI6yZo6" open="1"/>
<listitem type="829" id="tIHrr3vdvBFv" open="1"/>
<listitem type="829" id="hIRvrcjDBt2B" open="1"/>
<listitem type="829" id="tM1MfT3dGbDn" open="1"/>
<listitem type="829" id="xwL6qLHheXWT" open="1"/>
<listitem type="829" id="yRn8f2wKINF9" open="1"/>
<listitem type="829" id="jLVTWBskpZFo" open="1"/>
<listitem type="829" id="aS7VdKRbkCiP" open="1"/>
<listitem type="829" id="GJMsqsMRIRuv" open="1"/>
<listitem type="829" id="EejU1wcwrx2h" open="1"/>
</listitem>
<listitem type="817" id="VA6qSCiBtNc5" open="1"/>
<listitem type="817" id="AKxHpDCgOPhk" open="1"/>
</listitem>
<listitem type="802" id="Use Case View" open="1"/>
</listitem>
</listview>
<codegeneration>
<codegenerator language="C++"/>
</codegeneration>
</XMI.extensions>
</XMI>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -15,19 +15,15 @@
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${GB_LOGFILES_DIR}/gadgetbridge-%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>
<fileNamePattern>${GB_LOGFILES_DIR}/gadgetbridge-%d{yyyy-MM-dd}.log.zip</fileNamePattern>
<maxHistory>10</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- or whenever the file size reaches 50MB -->
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<!--<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>-->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{1} - %msg%n</pattern>
<!-- to debug crashes, set immediateFlush to true, otherwise keep it false to improve throughput -->
<immediateFlush>false</immediateFlush>
<!--<pattern>%date [%thread] %-5level %logger{25} - %msg%n</pattern>-->
</encoder>
</appender>

View File

@ -1,249 +1,97 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Normano64
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;
import android.annotation.TargetApi;
import android.app.Application;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.PhoneLookup;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.util.TypedValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import nodomain.freeyourgadget.gadgetbridge.database.ActivityDatabaseHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
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.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
/**
* Main Application class that initializes and provides access to certain things like
* logging and DB access.
*/
public class GBApplication extends Application {
// Since this class must not log to slf4j, we use plain android.util.Log
private static final String TAG = "GBApplication";
public static final String DATABASE_NAME = "Gadgetbridge";
private static GBApplication context;
private static ActivityDatabaseHandler mActivityDatabaseHandler;
private static final Lock dbLock = new ReentrantLock();
private static DeviceService deviceService;
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 2;
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
private static Prefs prefs;
private static GBPrefs gbPrefs;
private static LockHandler lockHandler;
/**
* Note: is null on Lollipop and Kitkat
*/
private static NotificationManager notificationManager;
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change";
private static GBApplication app;
private static Logging logging = new Logging() {
@Override
protected String createLogDirectory() throws IOException {
if (GBEnvironment.env().isLocalTest()) {
return System.getProperty(Logging.PROP_LOGFILES_DIR);
} else {
File dir = FileUtils.getExternalFilesDir();
return dir.getAbsolutePath();
}
}
};
private static Locale language;
private DeviceManager deviceManager;
public static void quit() {
GB.log("Quitting Gadgetbridge...", GB.INFO, null);
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
GBApplication.deviceService().quit();
}
public GBApplication() {
context = this;
deviceService = createDeviceService();
// 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) {
// guard against multiple invocations (robolectric)
return;
}
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
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());
setupLogging();
// For debugging problems with the logback configuration
// LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
// print logback's internal status
// StatusPrinter.print(lc);
// Logger logger = LoggerFactory.getLogger(GBApplication.class);
if (getPrefsFileVersion() != CURRENT_PREFS_VERSION) {
migratePrefs(getPrefsFileVersion());
}
setupExceptionHandler();
deviceManager = new DeviceManager(this);
String language = prefs.getString("language", "default");
setLanguage(language);
deviceService = createDeviceService();
loadAppsBlackList();
loadCalendarsBlackList();
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));
}
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= TRIM_MEMORY_BACKGROUND) {
if (!hasBusyDevice()) {
DBHelper.clearSession();
}
}
}
/**
* Returns true if at least a single device is busy, e.g synchronizing activity data
* or something similar.
* Note: busy is not the same as connected or initialized!
*/
private boolean hasBusyDevice() {
List<GBDevice> devices = getDeviceManager().getDevices();
for (GBDevice device : devices) {
if (device.isBusy()) {
return true;
}
}
return false;
}
public static void setupLogging(boolean enabled) {
logging.setupLogging(enabled);
}
private void setupExceptionHandler() {
LoggingExceptionHandler handler = new LoggingExceptionHandler(Thread.getDefaultUncaughtExceptionHandler());
Thread.setDefaultUncaughtExceptionHandler(handler);
GB.environment = GBEnvironment.createDeviceEnvironment();
mActivityDatabaseHandler = new ActivityDatabaseHandler(context);
// for testing DB stuff
// SQLiteDatabase db = mActivityDatabaseHandler.getWritableDatabase();
// db.close();
}
public static boolean isFileLoggingEnabled() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext());
return prefs.getBoolean("log_to_file", false);
}
public static boolean minimizeNotification() {
return prefs.getBoolean("minimize_priority", false);
}
public void setupDatabase() {
DaoMaster.OpenHelper helper;
GBEnvironment env = GBEnvironment.env();
if (env.isTest()) {
helper = new DaoMaster.DevOpenHelper(this, null, null);
private void setupLogging() {
if (isFileLoggingEnabled()) {
try {
File dir = FileUtils.getExternalFilesDir();
// used by assets/logback.xml since the location cannot be statically determined
System.setProperty("GB_LOGFILES_DIR", dir.getAbsolutePath());
} catch (IOException ex) {
Log.e("GBApplication", "External files dir not available, cannot log to file, ex");
System.setProperty("GB_LOGFILES_DIR", "/dev/null");
}
} else {
helper = new DBOpenHelper(this, DATABASE_NAME, null);
try {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.detachAppender("FILE");
} catch (Throwable ex) {
System.out.println("Error removing logger FILE appender");
ex.printStackTrace();
}
}
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
if (lockHandler == null) {
lockHandler = new LockHandler();
}
lockHandler.init(daoMaster, helper);
}
public static Context getContext() {
return context;
}
/**
* Returns the facade for talking to devices. Devices are managed by
* an Android Service and this facade provides access to its functionality.
*
* @return the facade for talking to the service/devices.
*/
public static DeviceService deviceService() {
return deviceService;
}
@ -253,18 +101,14 @@ public class GBApplication extends Application {
* when that was not successful
* If acquiring was successful, callers must call #releaseDB when they
* are done (from the same thread that acquired the lock!
* <p>
* Callers must not hold a reference to the returned instance because it
* will be invalidated at some point.
*
* @return the DBHandler
* @throws GBException
* @see #releaseDB()
* @throws GBException
*/
public static DBHandler acquireDB() throws GBException {
try {
if (dbLock.tryLock(30, TimeUnit.SECONDS)) {
return lockHandler;
return mActivityDatabaseHandler;
}
} catch (InterruptedException ex) {
Log.i(TAG, "Interrupted while waiting for DB lock");
@ -274,7 +118,6 @@ public class GBApplication extends Application {
/**
* Releases the database lock.
*
* @throws IllegalMonitorStateException if the current thread is not owning the lock
* @see #acquireDB()
*/
@ -285,310 +128,4 @@ public class GBApplication extends Application {
public static boolean isRunningLollipopOrLater() {
return VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
public static boolean isRunningMarshmallowOrLater() {
return VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
private static boolean isPrioritySender(int prioritySenders, String number) {
if (prioritySenders == Policy.PRIORITY_SENDERS_ANY) {
return true;
} else {
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
String[] projection = new String[]{PhoneLookup._ID, PhoneLookup.STARRED};
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
boolean exists = false;
int starred = 0;
try {
if (cursor != null && cursor.moveToFirst()) {
exists = true;
starred = cursor.getInt(cursor.getColumnIndexOrThrow(PhoneLookup.STARRED));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (prioritySenders == Policy.PRIORITY_SENDERS_CONTACTS && exists) {
return true;
} else if (prioritySenders == Policy.PRIORITY_SENDERS_STARRED && starred == 1) {
return true;
}
return false;
}
}
@TargetApi(Build.VERSION_CODES.M)
public static boolean isPriorityNumber(int priorityType, String number) {
NotificationManager.Policy notificationPolicy = notificationManager.getNotificationPolicy();
if (priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) == Policy.PRIORITY_CATEGORY_MESSAGES) {
return isPrioritySender(notificationPolicy.priorityMessageSenders, number);
}
} else if (priorityType == Policy.PRIORITY_CATEGORY_CALLS) {
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) == Policy.PRIORITY_CATEGORY_CALLS) {
return isPrioritySender(notificationPolicy.priorityCallSenders, number);
}
}
return false;
}
@TargetApi(Build.VERSION_CODES.M)
public static int getGrantedInterruptionFilter() {
if (prefs.getBoolean("notification_filter", false) && GBApplication.isRunningMarshmallowOrLater()) {
if (notificationManager.isNotificationPolicyAccessGranted()) {
return notificationManager.getCurrentInterruptionFilter();
}
}
return NotificationManager.INTERRUPTION_FILTER_ALL;
}
private static HashSet<String> apps_blacklist = null;
public static boolean appIsBlacklisted(String packageName) {
if (apps_blacklist == null) {
GB.log("appIsBlacklisted: apps_blacklist is null!", GB.INFO, null);
}
return apps_blacklist != null && apps_blacklist.contains(packageName);
}
public static void setAppsBlackList(Set<String> packageNames) {
if (packageNames == null) {
GB.log("Set null apps_blacklist", GB.INFO, null);
apps_blacklist = new HashSet<>();
} else {
apps_blacklist = new HashSet<>(packageNames);
}
GB.log("New apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
saveAppsBlackList();
}
private static void loadAppsBlackList() {
GB.log("Loading apps_blacklist", GB.INFO, null);
apps_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (apps_blacklist == null) {
apps_blacklist = new HashSet<>();
}
GB.log("Loaded apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
}
private static void saveAppsBlackList() {
GB.log("Saving apps_blacklist with " + apps_blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (apps_blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
} else {
Prefs.putStringSet(editor, GBPrefs.PACKAGE_BLACKLIST, apps_blacklist);
}
editor.apply();
}
public static void addAppToBlacklist(String packageName) {
if (apps_blacklist.add(packageName)) {
saveAppsBlackList();
}
}
public static synchronized void removeFromAppsBlacklist(String packageName) {
GB.log("Removing from apps_blacklist: " + packageName, GB.INFO, null);
apps_blacklist.remove(packageName);
saveAppsBlackList();
}
private static HashSet<String> calendars_blacklist = null;
public static boolean calendarIsBlacklisted(String calendarDisplayName) {
if (calendars_blacklist == null) {
GB.log("calendarIsBlacklisted: calendars_blacklist is null!", GB.INFO, null);
}
return calendars_blacklist != null && calendars_blacklist.contains(calendarDisplayName);
}
public static void setCalendarsBlackList(Set<String> calendarNames) {
if (calendarNames == null) {
GB.log("Set null apps_blacklist", GB.INFO, null);
calendars_blacklist = new HashSet<>();
} else {
calendars_blacklist = new HashSet<>(calendarNames);
}
GB.log("New calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
saveCalendarsBlackList();
}
public static void addCalendarToBlacklist(String calendarDisplayName) {
if (calendars_blacklist.add(calendarDisplayName)) {
saveCalendarsBlackList();
}
}
public static void removeFromCalendarBlacklist(String calendarDisplayName) {
calendars_blacklist.remove(calendarDisplayName);
saveCalendarsBlackList();
}
private static void loadCalendarsBlackList() {
GB.log("Loading calendars_blacklist", GB.INFO, null);
calendars_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
if (calendars_blacklist == null) {
calendars_blacklist = new HashSet<>();
}
GB.log("Loaded calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
}
private static void saveCalendarsBlackList() {
GB.log("Saving calendars_blacklist with " + calendars_blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (calendars_blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
} else {
Prefs.putStringSet(editor, GBPrefs.CALENDAR_BLACKLIST, calendars_blacklist);
}
editor.apply();
}
/**
* Deletes both the old Activity database and the new one recreates it with empty tables.
*
* @return true on successful deletion
*/
public static synchronized boolean deleteActivityDatabase(Context context) {
// TODO: flush, close, reopen db
if (lockHandler != null) {
lockHandler.closeDb();
}
boolean result = deleteOldActivityDatabase(context);
result &= getContext().deleteDatabase(DATABASE_NAME);
return result;
}
/**
* Deletes the legacy (pre 0.12) Activity database
*
* @return true on successful deletion
*/
public static synchronized boolean deleteOldActivityDatabase(Context context) {
DBHelper dbHelper = new DBHelper(context);
boolean result = true;
if (dbHelper.existsDB("ActivityDatabase")) {
result = getContext().deleteDatabase("ActivityDatabase");
}
return result;
}
private int getPrefsFileVersion() {
try {
return Integer.parseInt(sharedPrefs.getString(PREFS_VERSION, "0")); //0 is legacy
} catch (Exception e) {
//in version 1 this was an int
return 1;
}
}
private void migratePrefs(int oldVersion) {
SharedPreferences.Editor editor = sharedPrefs.edit();
switch (oldVersion) {
case 0:
String legacyGender = sharedPrefs.getString("mi_user_gender", null);
String legacyHeight = sharedPrefs.getString("mi_user_height_cm", null);
String legacyWeigth = sharedPrefs.getString("mi_user_weight_kg", null);
String legacyYOB = sharedPrefs.getString("mi_user_year_of_birth", null);
if (legacyGender != null) {
int gender = "male".equals(legacyGender) ? 1 : "female".equals(legacyGender) ? 0 : 2;
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(gender));
editor.remove("mi_user_gender");
}
if (legacyHeight != null) {
editor.putString(ActivityUser.PREF_USER_HEIGHT_CM, legacyHeight);
editor.remove("mi_user_height_cm");
}
if (legacyWeigth != null) {
editor.putString(ActivityUser.PREF_USER_WEIGHT_KG, legacyWeigth);
editor.remove("mi_user_weight_kg");
}
if (legacyYOB != null) {
editor.putString(ActivityUser.PREF_USER_YEAR_OF_BIRTH, legacyYOB);
editor.remove("mi_user_year_of_birth");
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
break;
case 1:
//migrate the integer version of gender introduced in version 1 to a string value, needed for the way Android accesses the shared preferences
int legacyGender_1 = 2;
try {
legacyGender_1 = sharedPrefs.getInt(ActivityUser.PREF_USER_GENDER, 2);
} catch (Exception e) {
Log.e(TAG, "Could not access legacy activity gender", e);
}
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(legacyGender_1));
//also silently migrate the version to a string value
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
break;
}
editor.apply();
}
public static void setLanguage(String lang) {
if (lang.equals("default")) {
language = Resources.getSystem().getConfiguration().locale;
} else {
language = new Locale(lang);
}
updateLanguage(language);
}
public static void updateLanguage(Locale locale) {
AndroidUtils.setLanguage(context, locale);
Intent intent = new Intent();
intent.setAction(ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public static LimitedQueue getIDSenderLookup() {
return mIDSenderLookup;
}
public static boolean isDarkThemeEnabled() {
return prefs.getString("pref_key_theme", context.getString(R.string.pref_theme_value_light)).equals(context.getString(R.string.pref_theme_value_dark));
}
public static int getTextColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.textColorPrimary, typedValue, true);
return typedValue.data;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateLanguage(getLanguage());
}
public static int getBackgroundColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(android.R.attr.background, typedValue, true);
return typedValue.data;
}
public static Prefs getPrefs() {
return prefs;
}
public static GBPrefs getGBPrefs() {
return gbPrefs;
}
public DeviceManager getDeviceManager() {
return deviceManager;
}
public static GBApplication app() {
return app;
}
public static Locale getLanguage() {
return language;
}
}

View File

@ -1,29 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge;
/**
* 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;
@ -33,8 +10,9 @@ public class GBEnvironment {
return env;
}
static GBEnvironment createDeviceEnvironment() {
return new GBEnvironment();
public static GBEnvironment createDeviceEnvironment() {
GBEnvironment env = new GBEnvironment();
return env;
}
public final boolean isTest() {
@ -45,15 +23,4 @@ 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

@ -1,34 +1,15 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge;
public class GBException extends Exception {
public GBException(String message, Throwable cause) {
super(message, cause);
}
public GBException(String message) {
super(message);
}
public GBException(Throwable cause) {
super(cause);
}
public GBException() {
super();
}

View File

@ -1,116 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
/**
* Provides lowlevel access to the database.
*/
public class LockHandler implements DBHandler {
private DaoMaster daoMaster = null;
private DaoSession session = null;
private SQLiteOpenHelper helper = null;
public LockHandler() {
}
public void init(DaoMaster daoMaster, DaoMaster.OpenHelper helper) {
if (isValid()) {
throw new IllegalStateException("DB must be closed before initializing it again");
}
if (daoMaster == null) {
throw new IllegalArgumentException("daoMaster must not be null");
}
if (helper == null) {
throw new IllegalArgumentException("helper must not be null");
}
this.daoMaster = daoMaster;
this.helper = helper;
session = daoMaster.newSession();
if (session == null) {
throw new RuntimeException("Unable to create database session");
}
}
@Override
public DaoMaster getDaoMaster() {
return daoMaster;
}
private boolean isValid() {
return daoMaster != null;
}
private void ensureValid() {
if (!isValid()) {
throw new IllegalStateException("LockHandler is not in a valid state");
}
}
@Override
public void close() {
ensureValid();
GBApplication.releaseDB();
}
@Override
public synchronized void openDb() {
if (session != null) {
throw new IllegalStateException("session must be null");
}
// this will create completely new db instances and in turn update this handler through #init()
GBApplication.app().setupDatabase();
}
@Override
public synchronized void closeDb() {
if (session == null) {
throw new IllegalStateException("session must not be null");
}
session.clear();
session.getDatabase().close();
session = null;
helper = null;
daoMaster = null;
}
@Override
public SQLiteOpenHelper getHelper() {
ensureValid();
return helper;
}
@Override
public DaoSession getDaoSession() {
ensureValid();
return session;
}
@Override
public SQLiteDatabase getDatabase() {
ensureValid();
return daoMaster.getDatabase();
}
}

View File

@ -1,164 +0,0 @@
/* Copyright (C) 2016-2017 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;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.util.StatusPrinter;
public abstract class Logging {
public static final String PROP_LOGFILES_DIR = "GB_LOGFILES_DIR";
private FileAppender<ILoggingEvent> fileLogger;
public void setupLogging(boolean enable) {
try {
if (fileLogger == null) {
init();
}
if (enable) {
startFileLogger();
} else {
stopFileLogger();
}
getLogger().info("Gadgetbridge version: " + BuildConfig.VERSION_NAME);
} catch (IOException ex) {
Log.e("GBApplication", "External files dir not available, cannot log to file", ex);
stopFileLogger();
}
}
public void debugLoggingConfiguration() {
// For debugging problems with the logback configuration
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
// print logback's internal status
StatusPrinter.print(lc);
// Logger logger = LoggerFactory.getLogger(Logging.class);
}
protected abstract String createLogDirectory() throws IOException;
protected void init() throws IOException {
String dir = createLogDirectory();
if (dir == null) {
throw new IllegalArgumentException("log directory must not be null");
}
// used by assets/logback.xml since the location cannot be statically determined
System.setProperty(PROP_LOGFILES_DIR, dir);
rememberFileLogger();
}
private Logger getLogger() {
return LoggerFactory.getLogger(Logging.class);
}
private void startFileLogger() {
if (fileLogger != null && !fileLogger.isStarted()) {
addFileLogger(fileLogger);
fileLogger.setLazy(false); // hack to make sure that start() actually opens the file
fileLogger.start();
}
}
private void stopFileLogger() {
if (fileLogger != null && fileLogger.isStarted()) {
fileLogger.stop();
removeFileLogger(fileLogger);
}
}
private void rememberFileLogger() {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
fileLogger = (FileAppender<ILoggingEvent>) root.getAppender("FILE");
}
private void addFileLogger(Appender<ILoggingEvent> fileLogger) {
try {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
if (!root.isAttached(fileLogger)) {
root.addAppender(fileLogger);
}
} catch (Throwable ex) {
Log.e("GBApplication", "Error adding logger FILE appender", ex);
}
}
private void removeFileLogger(Appender<ILoggingEvent> fileLogger) {
try {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
if (root.isAttached(fileLogger)) {
root.detachAppender(fileLogger);
}
} catch (Throwable ex) {
Log.e("GBApplication", "Error removing logger FILE appender", ex);
}
}
public FileAppender<ILoggingEvent> getFileLogger() {
return fileLogger;
}
public boolean setImmediateFlush(boolean enable) {
FileAppender<ILoggingEvent> fileLogger = getFileLogger();
Encoder<ILoggingEvent> encoder = fileLogger.getEncoder();
if (encoder instanceof LayoutWrappingEncoder) {
((LayoutWrappingEncoder) encoder).setImmediateFlush(enable);
return true;
}
return false;
}
public boolean isImmediateFlush() {
FileAppender<ILoggingEvent> fileLogger = getFileLogger();
Encoder<ILoggingEvent> encoder = fileLogger.getEncoder();
if (encoder instanceof LayoutWrappingEncoder) {
return ((LayoutWrappingEncoder) encoder).isImmediateFlush();
}
return false;
}
public static String formatBytes(byte[] bytes) {
if (bytes == null) {
return "(null)";
}
StringBuilder builder = new StringBuilder(bytes.length * 5);
for (byte b : bytes) {
builder.append(String.format("0x%2x", b));
builder.append(" ");
}
return builder.toString().trim();
}
public static void logBytes(Logger logger, byte[] value) {
if (value != null) {
for (byte b : value) {
logger.warn("DATA: " + String.format("0x%2x", b));
}
}
}
}

View File

@ -1,43 +0,0 @@
/* Copyright (C) 2015-2017 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Catches otherwise uncaught exceptions, logs them and terminates the app.
*/
public class LoggingExceptionHandler implements Thread.UncaughtExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(LoggingExceptionHandler.class);
private final Thread.UncaughtExceptionHandler mDelegate;
public LoggingExceptionHandler(Thread.UncaughtExceptionHandler delegate) {
mDelegate = delegate;
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
LOG.error("Uncaught exception: " + ex.getMessage(), ex);
if (mDelegate != null) {
mDelegate.uncaughtException(thread, ex);
} else {
System.exit(1);
}
}
}

View File

@ -1,131 +0,0 @@
/* Copyright (C) 2016-2017 0nse, 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;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.widget.RemoteViews;
import android.widget.Toast;
import java.util.Calendar;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* Implementation of SleepAlarmWidget functionality. When pressing the widget, an alarm will be set
* to trigger after a predefined number of hours. A toast will confirm the user about this. The
* value is retrieved using ActivityUser.().getSleepDuration().
*/
public class SleepAlarmWidget extends AppWidgetProvider {
/**
* This is our dedicated action to detect when the widget has been clicked.
*/
public static final String ACTION =
"nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK";
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.sleep_alarm_widget);
// Add our own click intent
Intent intent = new Intent(ACTION);
PendingIntent clickPI = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.sleepalarmwidget_text, clickPI);
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (ACTION.equals(intent.getAction())) {
int userSleepDuration = new ActivityUser().getSleepDuration();
// current timestamp
GregorianCalendar calendar = new GregorianCalendar();
// add preferred sleep duration
calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration);
// overwrite the first alarm and activate it
GBAlarm alarm = GBAlarm.createSingleShot(0, true, calendar);
alarm.store();
if (GBApplication.isRunningLollipopOrLater()) {
setAlarmViaAlarmManager(context, calendar.getTimeInMillis());
}
int hours = calendar.get(Calendar.HOUR_OF_DAY);
int minutes = calendar.get(Calendar.MINUTE);
GB.toast(context,
String.format(context.getString(R.string.appwidget_alarms_set), hours, minutes),
Toast.LENGTH_SHORT, GB.INFO);
}
}
/**
* Use the Android alarm manager to create the alarm icon in the status bar.
*
* @param packageContext {@code Context}: A Context of the application package implementing this
* class.
* @param triggerTime {@code long}: time at which the underlying alarm is triggered in wall time
* milliseconds since the epoch
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setAlarmViaAlarmManager(Context packageContext, long triggerTime) {
AlarmManager am = (AlarmManager) packageContext.getSystemService(Context.ALARM_SERVICE);
// TODO: launch the alarm configuration activity when clicking the alarm in the status bar
Intent intent = new Intent(packageContext, ConfigureAlarms.class);
PendingIntent pi = PendingIntent.getBroadcast(packageContext, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
am.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, pi), pi);
}
}

View File

@ -1,19 +1,3 @@
/* Copyright (C) 2015-2017 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.activities;
import android.support.v4.app.FragmentManager;
@ -24,7 +8,7 @@ import java.util.HashSet;
import java.util.Set;
public abstract class AbstractFragmentPagerAdapter extends FragmentStatePagerAdapter {
private final Set<AbstractGBFragment> fragments = new HashSet<>();
private Set<AbstractGBFragment> fragments = new HashSet<>();
private Object primaryFragment;
public AbstractFragmentPagerAdapter(FragmentManager fm) {

View File

@ -1,109 +0,0 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
public abstract class AbstractGBActivity extends AppCompatActivity implements GBActivity {
private boolean isLanguageInvalid = false;
public static final int NONE = 0;
public static final int NO_ACTIONBAR = 1;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage(), true);
break;
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
public void setLanguage(Locale language, boolean invalidateLanguage) {
if (invalidateLanguage) {
isLanguageInvalid = true;
}
AndroidUtils.setLanguage(this, language);
}
public static void init(GBActivity activity) {
init(activity, NONE);
}
public static void init(GBActivity activity, int flags) {
if (GBApplication.isDarkThemeEnabled()) {
if ((flags & NO_ACTIONBAR) != 0) {
activity.setTheme(R.style.GadgetbridgeThemeDark_NoActionBar);
} else {
activity.setTheme(R.style.GadgetbridgeThemeDark);
}
} else {
if ((flags & NO_ACTIONBAR) != 0) {
activity.setTheme(R.style.GadgetbridgeTheme_NoActionBar);
} else {
activity.setTheme(R.style.GadgetbridgeTheme);
}
}
activity.setLanguage(GBApplication.getLanguage(), false);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
init(this);
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (isLanguageInvalid) {
isLanguageInvalid = false;
recreate();
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -1,34 +1,17 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, walkjivefly
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
/**
* Abstract base class for fragments. Provides hooks that are called when
* the fragment is made visible and invisible in the activity. also allows
* the fragment to define the title to be shown in the activity.
*
* @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,16 +20,16 @@ public abstract class AbstractGBFragment extends Fragment {
* @see #onMadeInvisibleInActivity()
*/
protected void onMadeVisibleInActivity() {
updateActivityTitle();
}
/**
* Called when this fragment has been scrolled out of the activity.
*
* @see #isVisibleInActivity()
* @see #onMadeVisibleInActivity()
*/
protected void onMadeInvisibleInActivity() {
mVisibleInActivity = false;
mVisibleInactivity = false;
}
/**
@ -54,7 +37,16 @@ public abstract class AbstractGBFragment extends Fragment {
* activity, not taking into account whether the screen is enabled at all.
*/
public boolean isVisibleInActivity() {
return mVisibleInActivity;
return mVisibleInactivity;
}
protected void updateActivityTitle() {
FragmentActivity activity = (FragmentActivity) getActivity();
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
if (getTitle() != null) {
activity.setTitle(getTitle());
}
}
}
@Nullable
@ -62,11 +54,10 @@ public abstract class AbstractGBFragment extends Fragment {
/**
* Internal
*
* @hide
*/
public void onMadeVisibleInActivityInternal() {
mVisibleInActivity = true;
mVisibleInactivity = true;
if (isVisible()) {
onMadeVisibleInActivity();
}

View File

@ -1,30 +1,20 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.Context;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.util.AttributeSet;
import android.view.View;
/**
* A base activity that supports paging through fragments by swiping.
* Subclasses will have to add a ViewPager to their layout and add something
* like this to hook it to the fragments:
* <p/>
*
* <pre>
* // Set up the ViewPager with the sections adapter.
* ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
@ -33,7 +23,7 @@ import android.support.v4.app.FragmentPagerAdapter;
*
* @see AbstractGBFragment
*/
public abstract class AbstractGBFragmentActivity extends AbstractGBActivity {
public abstract class AbstractGBFragmentActivity extends FragmentActivity {
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* fragments for each of the sections. We use a
@ -60,7 +50,6 @@ public abstract class AbstractGBFragmentActivity extends AbstractGBActivity {
/**
* Creates a PagerAdapter that will create the fragments to be used with this
* activity. The fragments should typically extend AbstractGBFragment
*
* @param fragmentManager
* @return
*/

View File

@ -1,72 +1,26 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.text.InputType;
import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
/**
* A settings activity with support for preferences directly displaying their value.
* If you combine such preferences with a custom OnPreferenceChangeListener, you have
* to set that listener in #onCreate, *not* in #onPostCreate, otherwise the value will
* not be displayed.
*/
public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivity implements GBActivity {
public class AbstractSettingsActivity extends PreferenceActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class);
private boolean isLanguageInvalid = false;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage(), true);
break;
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
@ -74,20 +28,12 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
private static class SimpleSetSummaryOnChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
if (preference instanceof EditTextPreference) {
if ((((EditTextPreference) preference).getEditText().getKeyListener().getInputType() & InputType.TYPE_CLASS_NUMBER) != 0) {
if ("".equals(String.valueOf(value))) {
// reject empty numeric input
return false;
}
}
}
updateSummary(preference, value);
return true;
}
public void updateSummary(Preference preference, Object value) {
String stringValue = String.valueOf(value);
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
@ -110,15 +56,15 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
private static class ExtraSetSummaryOnChangeListener extends SimpleSetSummaryOnChangeListener {
private final Preference.OnPreferenceChangeListener prefChangeListener;
private Preference.OnPreferenceChangeListener delegate;
public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener prefChangeListener) {
this.prefChangeListener = prefChangeListener;
public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener delegate) {
this.delegate = delegate;
}
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
boolean result = prefChangeListener.onPreferenceChange(preference, value);
boolean result = delegate.onPreferenceChange(preference, value);
if (result) {
return super.onPreferenceChange(preference, value);
}
@ -126,24 +72,14 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
}
private static final SimpleSetSummaryOnChangeListener sBindPreferenceSummaryToValueListener = new SimpleSetSummaryOnChangeListener();
@Override
protected void onCreate(Bundle savedInstanceState) {
AbstractGBActivity.init(this);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
super.onCreate(savedInstanceState);
}
private static SimpleSetSummaryOnChangeListener sBindPreferenceSummaryToValueListener = new SimpleSetSummaryOnChangeListener();
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getActionBar().setDisplayHomeAsUpEnabled(true);
for (String prefKey : getPreferenceKeysWithSummary()) {
final Preference pref = findPreference(prefKey);
if (pref != null) {
@ -154,21 +90,6 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
}
@Override
protected void onResume() {
super.onResume();
if (isLanguageInvalid) {
isLanguageInvalid = false;
recreate();
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
/**
* Subclasses should reimplement this to return the keys of those
* preferences which should print its values as a summary below the
@ -220,11 +141,4 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
return super.onOptionsItemSelected(item);
}
public void setLanguage(Locale language, boolean invalidateLanguage) {
if (invalidateLanguage) {
isLanguageInvalid = true;
}
AndroidUtils.setLanguage(this, language);
}
}

View File

@ -1,117 +1,53 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.format.DateFormat;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckedTextView;
import android.widget.CheckBox;
import android.widget.TimePicker;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class AlarmDetails extends AbstractGBActivity {
public class AlarmDetails extends Activity {
private GBAlarm alarm;
private TimePicker timePicker;
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 CheckBox cbSmartWakeup;
private CheckBox cbMonday;
private CheckBox cbTuesday;
private CheckBox cbWednesday;
private CheckBox cbThursday;
private CheckBox cbFriday;
private CheckBox cbSaturday;
private CheckBox cbSunday;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alarm_details);
alarm = getIntent().getParcelableExtra("alarm");
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
Parcelable p = getIntent().getExtras().getParcelable("alarm");
alarm = (GBAlarm) p;
timePicker = (TimePicker) findViewById(R.id.alarm_time_picker);
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();
}
});
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);
timePicker.setIs24HourView(DateFormat.is24HourFormat(GBApplication.getContext()));
timePicker.setCurrentHour(alarm.getHour());
timePicker.setCurrentMinute(alarm.getMinute());
cbSmartWakeup.setChecked(alarm.isSmartWakeup());
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
cbSmartWakeup.setVisibility(smartAlarmVisibility);
cbMonday.setChecked(alarm.getRepetition(GBAlarm.ALARM_MON));
cbTuesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_TUE));
@ -123,19 +59,12 @@ public class AlarmDetails extends AbstractGBActivity {
}
private boolean supportsSmartWakeup() {
if (device != null) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.supportsSmartWakeup(device);
}
return false;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// back button
updateAlarm();
finish();
return true;
}
@ -143,16 +72,10 @@ public class AlarmDetails extends AbstractGBActivity {
}
private void updateAlarm() {
alarm.setSmartWakeup(supportsSmartWakeup() && cbSmartWakeup.isChecked());
alarm.setSmartWakeup(cbSmartWakeup.isChecked());
alarm.setRepetition(cbMonday.isChecked(), cbTuesday.isChecked(), cbWednesday.isChecked(), cbThursday.isChecked(), cbFriday.isChecked(), cbSaturday.isChecked(), cbSunday.isChecked());
alarm.setHour(timePicker.getCurrentHour());
alarm.setMinute(timePicker.getCurrentMinute());
alarm.store();
}
@Override
protected void onPause() {
updateAlarm();
super.onPause();
}
}

View File

@ -1,26 +1,11 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.R;
public class AndroidPairingActivity extends AbstractGBActivity {
public class AndroidPairingActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {

View File

@ -1,67 +1,142 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.support.v4.content.LocalBroadcastManager;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
public class AppBlacklistActivity extends AbstractGBActivity {
public class AppBlacklistActivity extends Activity {
private static final Logger LOG = LoggerFactory.getLogger(AppBlacklistActivity.class);
private AppBlacklistAdapter appBlacklistAdapter;
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(ControlCenter.ACTION_QUIT)) {
finish();
}
}
};
private SharedPreferences sharedPrefs;
private HashSet<String> blacklist = null;
private void loadBlackList() {
blacklist = (HashSet<String>) sharedPrefs.getStringSet("package_blacklist", null);
if (blacklist == null) {
blacklist = new HashSet<>();
}
}
private void saveBlackList() {
SharedPreferences.Editor editor = sharedPrefs.edit();
if (blacklist.isEmpty()) {
editor.putStringSet("package_blacklist", null);
} else {
editor.putStringSet("package_blacklist", blacklist);
}
editor.apply();
}
private synchronized void addToBlacklist(String packageName) {
if (!blacklist.contains(packageName)) {
blacklist.add(packageName);
saveBlackList();
}
}
private synchronized void removeFromBlacklist(String packageName) {
blacklist.remove(packageName);
saveBlackList();
}
@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));
getActionBar().setDisplayHomeAsUpEnabled(true);
appBlacklistAdapter = new AppBlacklistAdapter(R.layout.item_app_blacklist, this);
final PackageManager pm = getPackageManager();
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
appListView.setAdapter(appBlacklistAdapter);
loadBlackList();
SearchView searchView = (SearchView) findViewById(R.id.appListViewSearch);
searchView.setIconifiedByDefault(false);
searchView.setIconified(false);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
final List<ApplicationInfo> packageList = pm.getInstalledApplications(PackageManager.GET_META_DATA);
ListView appListView = (ListView) findViewById(R.id.appListView);
final ArrayAdapter<ApplicationInfo> adapter = new ArrayAdapter<ApplicationInfo>(this, R.layout.item_with_checkbox, packageList) {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
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(appInfo.loadLabel(pm));
deviceImageView.setImageDrawable(appInfo.loadIcon(pm));
if (blacklist.contains(appInfo.packageName)) {
checkbox.setChecked(true);
}
return view;
}
};
appListView.setAdapter(adapter);
appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public boolean onQueryTextChange(String newText) {
appBlacklistAdapter.getFilter().filter(newText);
return true;
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()) {
addToBlacklist(packageName);
} else {
removeFromBlacklist(packageName);
}
}
});
IntentFilter filter = new IntentFilter();
filter.addAction(ControlCenter.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
}
@Override
@ -73,4 +148,10 @@ public class AppBlacklistActivity extends AbstractGBActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -1,129 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.
*
* This technique can be used with an {@link android.app.Activity} class, not just
* {@link android.preference.PreferenceActivity}.
*/
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
public ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
public void setSupportActionBar(@Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
@Override
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}

View File

@ -0,0 +1,176 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class AppManagerActivity extends Activity {
public static final String ACTION_REFRESH_APPLIST
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(ControlCenter.ACTION_QUIT)) {
finish();
} else if (action.equals(ACTION_REFRESH_APPLIST)) {
appList.clear();
int appCount = intent.getIntExtra("app_count", 0);
for (Integer i = 0; i < appCount; i++) {
String appName = intent.getStringExtra("app_name" + i.toString());
String appCreator = intent.getStringExtra("app_creator" + i.toString());
UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
appList.add(new GBDeviceApp(uuid, appName, appCreator, "", appType));
}
if (sharedPrefs.getBoolean("pebble_force_untested", false)) {
appList.addAll(getSystemApps());
}
mGBDeviceAppAdapter.notifyDataSetChanged();
}
}
};
private SharedPreferences sharedPrefs;
private final List<GBDeviceApp> appList = new ArrayList<>();
private GBDeviceAppAdapter mGBDeviceAppAdapter;
private GBDeviceApp selectedApp = null;
private List<GBDeviceApp> getSystemApps() {
List<GBDeviceApp> systemApps = new ArrayList<>();
systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.UNKNOWN));
systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.UNKNOWN));
return systemApps;
}
private List<GBDeviceApp> getCachedApps() {
List<GBDeviceApp> cachedAppList = new ArrayList<>();
try {
File cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
File files[] = cachePath.listFiles();
if (files != null) {
for (File file : files) {
if (file.getName().endsWith(".pbw")) {
UUID uuid = UUID.fromString(file.getName().substring(0, file.getName().length() - 4));
cachedAppList.add(new GBDeviceApp(uuid, uuid.toString(), "N/A", "", GBDeviceApp.Type.UNKNOWN));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return cachedAppList;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
setContentView(R.layout.activity_appmanager);
getActionBar().setDisplayHomeAsUpEnabled(true);
ListView appListView = (ListView) findViewById(R.id.appListView);
mGBDeviceAppAdapter = new GBDeviceAppAdapter(this, appList);
appListView.setAdapter(this.mGBDeviceAppAdapter);
appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View v, int position, long id) {
UUID uuid = appList.get(position).getUUID();
GBApplication.deviceService().onAppStart(uuid);
}
});
registerForContextMenu(appListView);
appList.addAll(getCachedApps());
if (sharedPrefs.getBoolean("pebble_force_untested", false)) {
appList.addAll(getSystemApps());
}
IntentFilter filter = new IntentFilter();
filter.addAction(ControlCenter.ACTION_QUIT);
filter.addAction(ACTION_REFRESH_APPLIST);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
GBApplication.deviceService().onAppInfoReq();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(
R.menu.appmanager_context, menu);
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
selectedApp = appList.get(acmi.position);
menu.setHeaderTitle(selectedApp.getName());
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.appmanager_app_delete:
if (selectedApp != null) {
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
}
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -1,168 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.SeekBar;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.here.HereConstants;
import nodomain.freeyourgadget.gadgetbridge.entities.AudioEffect;
import nodomain.freeyourgadget.gadgetbridge.entities.AudioEffectType;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
public class AudioSettingsActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(AudioSettingsActivity.class);
private SeekBar seekBar;
private TextView volume_text;
// private boolean[] enabledEffects;
private int volume;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_settings);
LOG.debug("Create Audio Settings interface");
// FIXME: read enabled effects too
// FIXME: and EQ values XD
seekBar = (SeekBar) findViewById(R.id.volume_seekbar);
volume_text = (TextView) findViewById(R.id.volume_seekbar_volume);
int startingVolume = 30; // FIXME: how do I read it?
seekBar.setProgress(startingVolume);
setdB(startingVolume);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int position, boolean fromUser) {
// HERE's volume range is from 0xdb (-30dB on their app) to 0xff (+6dB)
// 0xff - 0xdb = 36 -> seekbar max value
// volume = seekbar + Min_volume (= 0xdb = 219)
volume = position + 219;
// LOG.debug("Volume = " + (byte)volume + " = " + volume + "= " + position);
AudioEffect eff = new AudioEffect(AudioEffectType.VOLUME, volume);
GBApplication.deviceService().onSetAudioProperty(eff);
// Show the volume on the UI in dB (range -30;6)
setdB(position);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// FIXME: we need to read the current value and display this.
// right now, I'm just showing 0 dB, Called after changeListener sets the value on
// the device too.
Map<Integer, AudioEffectType> switchIds = new HashMap<Integer, AudioEffectType>();
switchIds.put(R.id.audio_effect_echo, AudioEffectType.ECHO);
switchIds.put(R.id.audio_effect_bassboost, AudioEffectType.BASSBOOST);
switchIds.put(R.id.audio_effect_fuzz, AudioEffectType.FUZZ);
switchIds.put(R.id.audio_effect_flange, AudioEffectType.FLANGE);
switchIds.put(R.id.audio_effect_reverb, AudioEffectType.REVERB);
switchIds.put(R.id.audio_effect_noisemask, AudioEffectType.NOISEMASK);
switchIds.put(R.id.audio_effect_bitcrusher, AudioEffectType.BITCRUSHER);
switchIds.put(R.id.audio_effect_chorus, AudioEffectType.CHORUS);
for (int id : switchIds.keySet()) {
final AudioEffectType effect = switchIds.get(id);
Switch s = (Switch) findViewById(id);
s.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
LOG.info("Toggled " + effect.name());
applyEffect(new AudioEffect(effect, isChecked));
}
});
}
List<String> defaultPresets = new ArrayList<String>();
defaultPresets.add("8bit");
defaultPresets.add("Example");
RadioGroup presets = (RadioGroup)findViewById(R.id.audio_presets);
for (String preset : defaultPresets) {
RadioButton radioButton = new RadioButton(getBaseContext());
radioButton.setText(preset);
presets.addView(radioButton);
radioButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RadioButton r = (RadioButton) findViewById(getCheckedPreset());
applyPreset(r.getText().toString());
}
});
}
}
private int getCheckedPreset() {
return ((RadioGroup)findViewById(R.id.audio_presets)).getCheckedRadioButtonId();
}
private void setdB(int volume) {
volume_text.setText(" " + (volume - 30) + " dB");
}
private void applyPreset(String preset) {
LOG.info("Applying preset " + preset);
switch(preset) {
case "8bit":
LOG.debug("8bit");
ArrayList<Object> values = new ArrayList<Object>();
values.add("3byte");
values.add(256.0f); // bits
values.add(20000.0f); // freq
AudioEffect effect = new AudioEffect(AudioEffectType.BITCRUSHER,
true, values);
GBApplication.deviceService().onSetAudioProperty(effect);
break;
default:
LOG.error("Missing preset! (Programming error!?");
}
}
private void applyEffect(AudioEffect effect) {
GBApplication.deviceService().onSetAudioProperty(effect);
}
}

View File

@ -1,152 +0,0 @@
/* 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.activities;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class CalBlacklistActivity extends AbstractGBActivity {
private final String[] EVENT_PROJECTION = new String[]{
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.CALENDAR_COLOR
};
private ArrayList<Calendar> calendarsArrayList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calblacklist);
ListView calListView = (ListView) findViewById(R.id.calListView);
final Uri uri = CalendarContract.Calendars.CONTENT_URI;
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
GB.toast(this, "Calendar permission not granted. Nothing to do.", Toast.LENGTH_SHORT, GB.WARN);
return;
}
try (Cursor cur = getContentResolver().query(uri, EVENT_PROJECTION, null, null, null)) {
calendarsArrayList = new ArrayList<>();
while (cur != null && cur.moveToNext()) {
calendarsArrayList.add(new Calendar(cur.getString(0), cur.getInt(1)));
}
}
ArrayAdapter<Calendar> calAdapter = new CalendarListAdapter(this, calendarsArrayList);
calListView.setAdapter(calAdapter);
calListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int i, long id) {
Calendar item = calendarsArrayList.get(i);
CheckBox selected = (CheckBox) view.findViewById(R.id.item_checkbox);
toggleEntry(view);
if (selected.isChecked()) {
GBApplication.addCalendarToBlacklist(item.displayName);
} else {
GBApplication.removeFromCalendarBlacklist(item.displayName);
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void toggleEntry(View view) {
TextView name = (TextView) view.findViewById(R.id.calendar_name);
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
name.setPaintFlags(name.getPaintFlags() ^ Paint.STRIKE_THRU_TEXT_FLAG);
checked.toggle();
}
class Calendar {
private final String displayName;
private final int color;
public Calendar(String displayName, int color) {
this.displayName = displayName;
this.color = color;
}
}
private class CalendarListAdapter extends ArrayAdapter<Calendar> {
CalendarListAdapter(@NonNull Context context, @NonNull List<Calendar> calendars) {
super(context, 0, calendars);
}
@NonNull
@Override
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
Calendar item = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) super.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item_cal_blacklist, parent, false);
}
View color = view.findViewById(R.id.calendar_color);
TextView name = (TextView) view.findViewById(R.id.calendar_name);
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
if (GBApplication.calendarIsBlacklisted(item.displayName) && !checked.isChecked()) {
toggleEntry(view);
}
color.setBackgroundColor(item.color);
name.setText(item.displayName);
return view;
}
}
}

View File

@ -1,26 +1,10 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.ListActivity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.preference.PreferenceManager;
import android.view.MenuItem;
import java.util.Arrays;
@ -28,73 +12,51 @@ import java.util.HashSet;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBAlarmListAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS;
public class ConfigureAlarms extends AbstractGBActivity {
private static final int REQ_CONFIGURE_ALARM = 1;
public class ConfigureAlarms extends ListActivity {
private GBAlarmListAdapter mGBAlarmListAdapter;
private Set<String> preferencesAlarmListSet;
private boolean avoidSendAlarmsToDevice;
private GBDevice device;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_configure_alarms);
getActionBar().setDisplayHomeAsUpEnabled(true);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
if (preferencesAlarmListSet.isEmpty()) {
//initialize the preferences
preferencesAlarmListSet = new HashSet<>(Arrays.asList(GBAlarm.DEFAULT_ALARMS));
prefs.getPreferences().edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).apply();
sharedPrefs.edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).commit();
}
mGBAlarmListAdapter = new GBAlarmListAdapter(this, preferencesAlarmListSet);
RecyclerView alarmsRecyclerView = (RecyclerView) findViewById(R.id.alarm_list);
alarmsRecyclerView.setHasFixedSize(true);
alarmsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
alarmsRecyclerView.setAdapter(mGBAlarmListAdapter);
updateAlarmsFromPrefs();
setListAdapter(mGBAlarmListAdapter);
}
@Override
protected void onPause() {
if (!avoidSendAlarmsToDevice) {
sendAlarmsToDevice();
}
super.onPause();
}
protected void onResume() {
super.onResume();
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQ_CONFIGURE_ALARM) {
avoidSendAlarmsToDevice = false;
updateAlarmsFromPrefs();
}
}
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
private void updateAlarmsFromPrefs() {
Prefs prefs = GBApplication.getPrefs();
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
int reservedSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet, reservedSlots);
mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet);
mGBAlarmListAdapter.notifyDataSetChanged();
sendAlarmsToDevice();
}
@Override
@ -102,6 +64,7 @@ public class ConfigureAlarms extends AbstractGBActivity {
switch (item.getItemId()) {
case android.R.id.home:
// back button
sendAlarmsToDevice();
finish();
return true;
}
@ -109,15 +72,10 @@ public class ConfigureAlarms extends AbstractGBActivity {
}
public void configureAlarm(GBAlarm alarm) {
avoidSendAlarmsToDevice = true;
Intent startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
Intent startIntent;
startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
startIntent.putExtra("alarm", alarm);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
startActivityForResult(startIntent, REQ_CONFIGURE_ALARM);
}
private GBDevice getDevice() {
return device;
startActivity(startIntent);
}
private void sendAlarmsToDevice() {

View File

@ -0,0 +1,349 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ControlCenter extends Activity {
private static final Logger LOG = LoggerFactory.getLogger(ControlCenter.class);
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.controlcenter.action.quit";
public static final String ACTION_REFRESH_DEVICELIST
= "nodomain.freeyourgadget.gadgetbridge.controlcenter.action.set_version";
private TextView hintTextView;
private ListView deviceListView;
private GBDeviceAdapter mGBDeviceAdapter;
private GBDevice selectedDevice = null;
private final List<GBDevice> deviceList = new ArrayList<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case ACTION_QUIT:
finish();
break;
case ACTION_REFRESH_DEVICELIST:
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
refreshPairedDevices();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev.getAddress() != null) {
int index = deviceList.indexOf(dev); // search by address
if (index >= 0) {
deviceList.set(index, dev);
} else {
deviceList.add(dev);
}
}
refreshPairedDevices();
refreshBusyState(dev);
break;
}
}
};
private void refreshBusyState(GBDevice dev) {
mGBDeviceAdapter.notifyDataSetChanged();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_controlcenter);
hintTextView = (TextView) findViewById(R.id.hintTextView);
deviceListView = (ListView) findViewById(R.id.deviceListView);
mGBDeviceAdapter = new GBDeviceAdapter(this, deviceList);
deviceListView.setAdapter(this.mGBDeviceAdapter);
deviceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View v, int position, long id) {
GBDevice gbDevice = deviceList.get(position);
if (gbDevice.isConnected()) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
Class<? extends Activity> primaryActivity = coordinator.getPrimaryActivity();
if (primaryActivity != null) {
Intent startIntent = new Intent(ControlCenter.this, primaryActivity);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice);
startActivity(startIntent);
}
} else {
GBApplication.deviceService().connect(deviceList.get(position).getAddress());
}
}
});
registerForContextMenu(deviceListView);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(ACTION_QUIT);
filterLocal.addAction(ACTION_REFRESH_DEVICELIST);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filterLocal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
refreshPairedDevices();
/*
* Ask for permission to intercept notifications on first run.
*/
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPrefs.getBoolean("firstrun", true)) {
sharedPrefs.edit().putBoolean("firstrun", false).apply();
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
startActivity(enableIntent);
}
GBApplication.deviceService().start();
if (GB.isBluetoothEnabled() && deviceList.isEmpty()) {
// start discovery when no devices are present
startActivity(new Intent(this, DiscoveryActivity.class));
} else {
GBApplication.deviceService().requestDeviceInfo();
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
selectedDevice = deviceList.get(acmi.position);
if (selectedDevice != null && selectedDevice.isBusy()) {
// no context menu when device is busy
return;
}
getMenuInflater().inflate(R.menu.controlcenter_context, menu);
if (!selectedDevice.isConnected() || selectedDevice.getType() == DeviceType.PEBBLE) {
menu.removeItem(R.id.controlcenter_fetch_activity_data);
menu.removeItem(R.id.controlcenter_configure_alarms);
}
if (!selectedDevice.isConnected() || selectedDevice.getType() == DeviceType.MIBAND) {
menu.removeItem(R.id.controlcenter_take_screenshot);
}
if (selectedDevice.getState() == GBDevice.State.NOT_CONNECTED) {
menu.removeItem(R.id.controlcenter_disconnect);
}
if (!selectedDevice.isInitialized()) {
menu.removeItem(R.id.controlcenter_find_device);
}
menu.setHeaderTitle(selectedDevice.getName());
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.controlcenter_start_sleepmonitor:
if (selectedDevice != null) {
Intent startIntent;
startIntent = new Intent(ControlCenter.this, ChartsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, selectedDevice);
startActivity(startIntent);
}
return true;
case R.id.controlcenter_fetch_activity_data:
if (selectedDevice != null) {
GBApplication.deviceService().onFetchActivityData();
}
return true;
case R.id.controlcenter_disconnect:
if (selectedDevice != null) {
selectedDevice = null;
GBApplication.deviceService().disconnect();
}
return true;
case R.id.controlcenter_find_device:
if (selectedDevice != null) {
findDevice(true);
ProgressDialog.show(
this,
getString(R.string.control_center_find_lost_device),
getString(R.string.control_center_cancel_to_stop_vibration),
true, true,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
findDevice(false);
}
});
}
return true;
case R.id.controlcenter_configure_alarms:
if (selectedDevice != null) {
Intent startIntent;
startIntent = new Intent(ControlCenter.this, ConfigureAlarms.class);
startActivity(startIntent);
}
return true;
case R.id.controlcenter_take_screenshot:
if (selectedDevice != null) {
GBApplication.deviceService().onScreenshotReq();
}
return true;
default:
return super.onContextItemSelected(item);
}
}
private void findDevice(boolean start) {
GBApplication.deviceService().onFindDevice(start);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivity(settingsIntent);
return true;
case R.id.action_debug:
Intent debugIntent = new Intent(this, DebugActivity.class);
startActivity(debugIntent);
return true;
case R.id.action_quit:
GBApplication.deviceService().quit();
Intent quitIntent = new Intent(ControlCenter.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).sendBroadcast(quitIntent);
return true;
case R.id.action_discover:
Intent discoverIntent = new Intent(this, DiscoveryActivity.class);
startActivity(discoverIntent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
super.onDestroy();
}
private void refreshPairedDevices() {
boolean connected = false;
List<GBDevice> availableDevices = new ArrayList<>();
for (GBDevice device : deviceList) {
if (device.isConnected() || device.isConnecting()) {
connected = true;
availableDevices.add(device);
}
}
BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
if (btAdapter == null) {
Toast.makeText(this, R.string.bluetooth_is_not_supported_, Toast.LENGTH_SHORT).show();
} else if (!btAdapter.isEnabled()) {
Toast.makeText(this, R.string.bluetooth_is_disabled_, Toast.LENGTH_SHORT).show();
} else {
Set<BluetoothDevice> pairedDevices = btAdapter.getBondedDevices();
for (BluetoothDevice pairedDevice : pairedDevices) {
DeviceType deviceDeviceType;
if (pairedDevice.getName().indexOf("Pebble") == 0) {
deviceDeviceType = DeviceType.PEBBLE;
} else if (pairedDevice.getName().equals("MI")) {
deviceDeviceType = DeviceType.MIBAND;
} else {
continue;
}
GBDevice device = new GBDevice(pairedDevice.getAddress(), pairedDevice.getName(), deviceDeviceType);
if (!availableDevices.contains(device)) {
availableDevices.add(device);
}
}
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
String miAddr = sharedPrefs.getString(MiBandConst.PREF_MIBAND_ADDRESS, "");
if (miAddr.length() > 0) {
GBDevice miDevice = new GBDevice(miAddr, "MI", DeviceType.MIBAND);
if (!availableDevices.contains(miDevice)) {
availableDevices.add(miDevice);
}
}
String pebbleEmuAddr = sharedPrefs.getString("pebble_emu_addr", "");
String pebbleEmuPort = sharedPrefs.getString("pebble_emu_port", "");
if (pebbleEmuAddr.length() >= 7 && pebbleEmuPort.length() > 0) {
GBDevice pebbleEmuDevice = new GBDevice(pebbleEmuAddr + ":" + pebbleEmuPort, "Pebble qemu", DeviceType.PEBBLE);
if (!availableDevices.contains(pebbleEmuDevice)) {
availableDevices.add(pebbleEmuDevice);
}
}
deviceList.retainAll(availableDevices);
for (GBDevice dev : availableDevices) {
if (!deviceList.contains(dev)) {
deviceList.add(dev);
}
}
if (connected) {
hintTextView.setText(R.string.tap_connected_device_for_app_mananger);
} else if (!deviceList.isEmpty()) {
hintTextView.setText(R.string.tap_a_device_to_connect);
}
}
mGBDeviceAdapter.notifyDataSetChanged();
}
}

View File

@ -1,331 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, 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.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
//TODO: extend AbstractGBActivity, but it requires actionbar that is not available
public class ControlCenterv2 extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener, GBActivity {
//needed for KK compatibility
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
private DeviceManager deviceManager;
private ImageView background;
private List<GBDevice> deviceList;
private GBDeviceAdapterv2 mGBDeviceAdapter;
private RecyclerView deviceListView;
private boolean isLanguageInvalid = false;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage(), true);
break;
case GBApplication.ACTION_QUIT:
finish();
break;
case DeviceManager.ACTION_DEVICES_CHANGED:
refreshPairedDevices();
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_controlcenterv2);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchDiscoveryActivity();
}
});
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
drawer.setDrawerListener(toggle);
toggle.syncState();
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
//end of material design boilerplate
deviceManager = ((GBApplication) getApplication()).getDeviceManager();
deviceListView = (RecyclerView) findViewById(R.id.deviceListView);
deviceListView.setHasFixedSize(true);
deviceListView.setLayoutManager(new LinearLayoutManager(this));
background = (ImageView) findViewById(R.id.no_items_bg);
deviceList = deviceManager.getDevices();
mGBDeviceAdapter = new GBDeviceAdapterv2(this, deviceList);
deviceListView.setAdapter(this.mGBDeviceAdapter);
ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.LEFT , ItemTouchHelper.RIGHT) {
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if(dX>50)
dX = 50;
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
GB.toast(getBaseContext(), "onMove", Toast.LENGTH_LONG, GB.ERROR);
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
GB.toast(getBaseContext(), "onSwiped", Toast.LENGTH_LONG, GB.ERROR);
}
@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
}
});
//uncomment to enable fixed-swipe to reveal more actions
//swipeToDismissTouchHelper.attachToRecyclerView(deviceListView);
registerForContextMenu(deviceListView);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
refreshPairedDevices();
/*
* Ask for permission to intercept notifications on first run.
*/
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("firstrun", true)) {
prefs.getPreferences().edit().putBoolean("firstrun", false).apply();
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
startActivity(enableIntent);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkAndRequestPermissions();
}
ChangeLog cl = createChangeLog();
if (cl.isFirstRun()) {
cl.getLogDialog().show();
}
GBApplication.deviceService().start();
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
startActivity(new Intent(this, DiscoveryActivity.class));
} else {
GBApplication.deviceService().requestDeviceInfo();
}
}
@Override
protected void onResume() {
super.onResume();
if (isLanguageInvalid) {
isLanguageInvalid = false;
recreate();
}
}
@Override
protected void onDestroy() {
unregisterForContextMenu(deviceListView);
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
switch (item.getItemId()) {
case R.id.action_settings:
Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivity(settingsIntent);
return true;
case R.id.action_debug:
Intent debugIntent = new Intent(this, DebugActivity.class);
startActivity(debugIntent);
return true;
case R.id.action_db_management:
Intent dbIntent = new Intent(this, DbManagementActivity.class);
startActivity(dbIntent);
return true;
case R.id.action_quit:
GBApplication.quit();
return true;
case R.id.donation_link:
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
return true;
case R.id.external_changelog:
ChangeLog cl = createChangeLog();
cl.getFullLogDialog().show();
return true;
}
return true;
}
private ChangeLog createChangeLog() {
String css = ChangeLog.DEFAULT_CSS;
css += "body { "
+ "color: " + AndroidUtils.getTextColorHex(getBaseContext()) + "; "
+ "background-color: " + AndroidUtils.getBackgroundColorHex(getBaseContext()) + ";" +
"}";
return new ChangeLog(this, css);
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivity.class));
}
private void refreshPairedDevices() {
List<GBDevice> deviceList = deviceManager.getDevices();
if (deviceList.isEmpty()) {
background.setVisibility(View.VISIBLE);
} else {
background.setVisibility(View.INVISIBLE);
}
mGBDeviceAdapter.notifyDataSetChanged();
}
@TargetApi(Build.VERSION_CODES.M)
private void checkAndRequestPermissions() {
List<String> wantedPermissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.BLUETOOTH);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CONTACTS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.CALL_PHONE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_PHONE_STATE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.SEND_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CALENDAR);
if (!wantedPermissions.isEmpty())
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);
}
public void setLanguage(Locale language, boolean invalidateLanguage) {
if (invalidateLanguage) {
isLanguageInvalid = true;
}
AndroidUtils.setLanguage(this, language);
}
}

View File

@ -1,237 +0,0 @@
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, 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.activities;
import android.app.AlertDialog;
import android.content.DialogInterface;
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;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
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;
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 AbstractGBActivity {
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;
private Button deleteOldActivityDBButton;
private Button deleteDBButton;
private TextView dbPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_db_management);
dbPath = (TextView) findViewById(R.id.activity_db_management_path);
dbPath.setText(getExternalPath());
exportDBButton = (Button) findViewById(R.id.exportDBButton);
exportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportDB();
}
});
importDBButton = (Button) findViewById(R.id.importDBButton);
importDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importDB();
}
});
int oldDBVisibility = hasOldActivityDatabase() ? View.VISIBLE : View.GONE;
deleteOldActivityDBButton = (Button) findViewById(R.id.deleteOldActivityDB);
deleteOldActivityDBButton.setVisibility(oldDBVisibility);
deleteOldActivityDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteOldActivityDbFile();
}
});
deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
deleteDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
deleteActivityDatabase();
}
});
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
}
private boolean hasOldActivityDatabase() {
return new DBHelper(this).existsDB("ActivityDatabase");
}
private String getExternalPath() {
try {
return FileUtils.getExternalFilesDir().getAbsolutePath();
} catch (Exception ex) {
LOG.warn("Unable to get external files dir", ex);
}
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);
GB.toast(this, getString(R.string.dbmanagementactivity_exported_to, destFile.getAbsolutePath()), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
private void importDB() {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_import_data_title)
.setMessage(R.string.dbmanagementactivity_overwrite_database_confirmation)
.setPositiveButton(R.string.dbmanagementactivity_overwrite, new DialogInterface.OnClickListener() {
@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();
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
helper.importDB(dbHandler, sourceFile);
helper.validateDB(sqLiteOpenHelper);
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_import_successful), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
private void deleteActivityDatabase() {
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
.setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteActivityDatabase(DbManagementActivity.this)) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_database_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.show();
}
private void deleteOldActivityDbFile() {
new AlertDialog.Builder(this).setCancelable(true);
new AlertDialog.Builder(this).setTitle(R.string.dbmanagementactivity_delete_old_activity_db);
new AlertDialog.Builder(this).setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation);
new AlertDialog.Builder(this).setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (GBApplication.deleteOldActivityDatabase(DbManagementActivity.this)) {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
}
}
});
new AlertDialog.Builder(this).setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
new AlertDialog.Builder(this).show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -1,67 +1,41 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, ivanovlev, Kasha, Lem Dulfo, Steffen Liebergeld
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.io.File;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.model.ServiceCommand;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DebugActivity extends AbstractGBActivity {
public class DebugActivity extends Activity {
private static final Logger LOG = LoggerFactory.getLogger(DebugActivity.class);
private static final String EXTRA_REPLY = "reply";
private static final String ACTION_REPLY
= "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply";
private Spinner sendTypeSpinner;
private Button sendButton;
private Button sendSMSButton;
private Button sendEmailButton;
private Button incomingCallButton;
private Button outgoingCallButton;
private Button startCallButton;
@ -70,26 +44,15 @@ public class DebugActivity extends AbstractGBActivity {
private Button setMusicInfoButton;
private Button setTimeButton;
private Button rebootButton;
private Button HeartRateButton;
private Button testNewFunctionalityButton;
private Button exportDBButton;
private Button importDBButton;
private EditText editContent;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_REPLY: {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
CharSequence reply = remoteInput.getCharSequence(EXTRA_REPLY);
LOG.info("got wearable reply: " + reply);
GB.toast(context, "got wearable reply: " + reply, Toast.LENGTH_SHORT, GB.INFO);
break;
}
case DeviceService.ACTION_HEARTRATE_MEASUREMENT: {
int hrValue = intent.getIntExtra(DeviceService.EXTRA_HEART_RATE_VALUE, -1);
GB.toast(DebugActivity.this, "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO);
break;
}
if (intent.getAction().equals(ControlCenter.ACTION_QUIT)) {
finish();
}
}
};
@ -98,37 +61,26 @@ public class DebugActivity extends AbstractGBActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_debug);
getActionBar().setDisplayHomeAsUpEnabled(true);
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_REPLY);
filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
registerReceiver(mReceiver, filter); // for ACTION_REPLY
registerReceiver(mReceiver, new IntentFilter(ControlCenter.ACTION_QUIT));
editContent = (EditText) findViewById(R.id.editContent);
ArrayList<String> spinnerArray = new ArrayList<>();
for (NotificationType notificationType : NotificationType.values()) {
spinnerArray.add(notificationType.name());
}
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, spinnerArray);
sendTypeSpinner = (Spinner) findViewById(R.id.sendTypeSpinner);
sendTypeSpinner.setAdapter(spinnerArrayAdapter);
sendButton = (Button) findViewById(R.id.sendButton);
sendButton.setOnClickListener(new View.OnClickListener() {
sendSMSButton = (Button) findViewById(R.id.sendSMSButton);
sendSMSButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NotificationSpec notificationSpec = new NotificationSpec();
String testString = editContent.getText().toString();
notificationSpec.phoneNumber = testString;
notificationSpec.body = testString;
notificationSpec.sender = testString;
notificationSpec.subject = testString;
notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
notificationSpec.pebbleColor = notificationSpec.type.color;
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec);
GBApplication.deviceService().onSMS(getResources().getText(R.string.app_name).toString(), editContent.getText().toString());
}
});
sendEmailButton = (Button) findViewById(R.id.sendEmailButton);
sendEmailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GBApplication.deviceService().onEmail(
getResources().getText(R.string.app_name).toString(),
getResources().getText(R.string.test).toString(),
editContent.getText().toString());
}
});
@ -136,20 +88,20 @@ public class DebugActivity extends AbstractGBActivity {
incomingCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_INCOMING;
callSpec.number = editContent.getText().toString();
GBApplication.deviceService().onSetCallState(callSpec);
GBApplication.deviceService().onSetCallState(
editContent.getText().toString(),
null,
ServiceCommand.CALL_INCOMING);
}
});
outgoingCallButton = (Button) findViewById(R.id.outgoingCallButton);
outgoingCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_OUTGOING;
callSpec.number = editContent.getText().toString();
GBApplication.deviceService().onSetCallState(callSpec);
GBApplication.deviceService().onSetCallState(
editContent.getText().toString(),
null,
ServiceCommand.CALL_OUTGOING);
}
});
@ -157,18 +109,35 @@ public class DebugActivity extends AbstractGBActivity {
startCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_START;
GBApplication.deviceService().onSetCallState(callSpec);
GBApplication.deviceService().onSetCallState(
null,
null,
ServiceCommand.CALL_START);
}
});
endCallButton = (Button) findViewById(R.id.endCallButton);
endCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_END;
GBApplication.deviceService().onSetCallState(callSpec);
GBApplication.deviceService().onSetCallState(
null,
null,
ServiceCommand.CALL_END);
}
});
exportDBButton = (Button) findViewById(R.id.exportDBButton);
exportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportDB();
}
});
importDBButton = (Button) findViewById(R.id.importDBButton);
importDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
importDB();
}
});
@ -179,38 +148,15 @@ public class DebugActivity extends AbstractGBActivity {
GBApplication.deviceService().onReboot();
}
});
HeartRateButton = (Button) findViewById(R.id.HearRateButton);
HeartRateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GB.toast("Measuring heart rate, please wait...", Toast.LENGTH_LONG, GB.INFO);
GBApplication.deviceService().onHeartRateTest();
}
});
setMusicInfoButton = (Button) findViewById(R.id.setMusicInfoButton);
setMusicInfoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MusicSpec musicSpec = new MusicSpec();
String testString = editContent.getText().toString();
musicSpec.artist = testString + "(artist)";
musicSpec.album = testString + "(album)";
musicSpec.track = testString + "(track)";
musicSpec.duration = 10;
musicSpec.trackCount = 5;
musicSpec.trackNr = 2;
GBApplication.deviceService().onSetMusicInfo(musicSpec);
MusicStateSpec stateSpec = new MusicStateSpec();
stateSpec.position = 0;
stateSpec.state = 0x01; // playing
stateSpec.playRate = 100;
stateSpec.repeat = 1;
stateSpec.shuffle = 1;
GBApplication.deviceService().onSetMusicState(stateSpec);
GBApplication.deviceService().onSetMusicInfo(
editContent.getText().toString() + "(artist)",
editContent.getText().toString() + "(album)",
editContent.getText().toString() + "(tracl)");
}
});
@ -229,18 +175,44 @@ public class DebugActivity extends AbstractGBActivity {
testNotification();
}
});
testNewFunctionalityButton = (Button) findViewById(R.id.testNewFunctionality);
testNewFunctionalityButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testNewFunctionality();
}
});
}
private void testNewFunctionality() {
GBApplication.deviceService().onTestNewFunction();
private void exportDB() {
DBHandler dbHandler = null;
try {
dbHandler = GBApplication.acquireDB();
DBHelper helper = new DBHelper(this);
File dir = FileUtils.getExternalFilesDir();
File destFile = helper.exportDB(dbHandler.getHelper(), dir);
GB.toast(this, "Exported to: " + destFile.getAbsolutePath(), Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
LOG.error("Unable to export db", ex);
Toast.makeText(this, "Error exporting DB: " + ex.getMessage(), Toast.LENGTH_LONG).show();
} finally {
if (dbHandler != null) {
dbHandler.release();
}
}
}
private void importDB() {
DBHandler dbHandler = null;
try {
dbHandler = GBApplication.acquireDB();
DBHelper helper = new DBHelper(this);
File dir = FileUtils.getExternalFilesDir();
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
helper.importDB(sqLiteOpenHelper, sourceFile);
helper.validateDB(sqLiteOpenHelper);
GB.toast(this, "Import successful.", Toast.LENGTH_LONG, GB.INFO);
} catch (Exception ex) {
GB.toast(this, "Error importing DB: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR);
} finally {
if (dbHandler != null) {
dbHandler.release();
}
}
}
private void testNotification() {
@ -251,30 +223,13 @@ public class DebugActivity extends AbstractGBActivity {
notificationIntent, 0);
NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_REPLY)
.build();
Intent replyIntent = new Intent(ACTION_REPLY);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(this, 0, replyIntent, 0);
NotificationCompat.Action action =
new NotificationCompat.Action.Builder(android.R.drawable.ic_input_add, "Reply", replyPendingIntent)
.addRemoteInput(remoteInput)
.build();
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender().addAction(action);
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.test_notification))
.setContentText(getString(R.string.this_is_a_test_notification_from_gadgetbridge))
.setTicker(getString(R.string.this_is_a_test_notification_from_gadgetbridge))
.setSmallIcon(R.drawable.ic_notification)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.extend(wearableExtender);
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this);
ncomp.setContentTitle(getString(R.string.test_notification));
ncomp.setContentText(getString(R.string.this_is_a_test_notification_from_gadgetbridge));
ncomp.setTicker(getString(R.string.this_is_a_test_notification_from_gadgetbridge));
ncomp.setSmallIcon(R.drawable.ic_notification);
ncomp.setAutoCancel(true);
ncomp.setContentIntent(pendingIntent);
nManager.notify((int) System.currentTimeMillis(), ncomp.build());
}
@ -291,11 +246,7 @@ public class DebugActivity extends AbstractGBActivity {
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}
public interface DeviceSelectionCallback {
void invoke(GBDevice device);
}
}

View File

@ -1,47 +1,17 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, JohnnySun, Lem Dulfo, Uwe Hermann
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
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;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
@ -53,56 +23,35 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
import static android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY;
import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY;
public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener {
public class DiscoveryActivity extends Activity implements AdapterView.OnItemClickListener {
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class);
private static final long SCAN_DURATION = 60000; // 60s
private ScanCallback newLeScanCallback = null;
private Handler handler = new Handler();
private final Handler handler = new Handler();
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
private BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
if (isScanning != Scanning.SCANNING_BTLE && isScanning != Scanning.SCANNING_NEW_BTLE) {
discoveryStarted(Scanning.SCANNING_BT);
}
discoveryStarted(Scanning.SCANNING_BT);
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
handler.post(new Runnable() {
@Override
public void run() {
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
checkAndRequestLocationPermission();
if (GBApplication.isRunningLollipopOrLater()) {
startDiscovery(Scanning.SCANNING_NEW_BTLE);
} else {
startDiscovery(Scanning.SCANNING_BTLE);
}
} else {
discoveryFinished();
}
}
});
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
startDiscovery(Scanning.SCANNING_BTLE);
} else {
discoveryFinished();
}
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);
@ -115,20 +64,13 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
handleDeviceFound(device, rssi);
break;
}
case BluetoothDevice.ACTION_UUID: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
ParcelUuid[] uuids2 = AndroidUtils.toParcelUUids(uuids);
handleDeviceFound(device, rssi, uuids2);
break;
}
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null && bondingDevice != null && device.getAddress().equals(bondingDevice.getMacAddress())) {
if (device != null && device.getAddress().equals(bondingAddress)) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_BONDED) {
handleDeviceBonded();
LOG.info("Successfully bonded with: " + bondingAddress);
finish();
}
}
}
@ -136,109 +78,14 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
};
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() {
private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
logMessageContent(scanRecord);
handleDeviceFound(device, (short) rssi);
}
};
// why use a method to get callback?
// because this callback need API >= 21
// we cant add @TARGETAPI("Lollipop") at class header
// so use a method with SDK check to return this callback
private ScanCallback getScanCallback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
newLeScanCallback = new ScanCallback() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
try {
ScanRecord scanRecord = result.getScanRecord();
ParcelUuid[] uuids = null;
if (scanRecord != null) {
//logMessageContent(scanRecord.getBytes());
List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
if (serviceUuids != null) {
uuids = serviceUuids.toArray(new ParcelUuid[0]);
}
}
LOG.warn(result.getDevice().getName() + ": " +
((scanRecord != null) ? scanRecord.getBytes().length : -1));
handleDeviceFound(result.getDevice(), (short) result.getRssi(), uuids);
} catch (NullPointerException e) {
LOG.warn("Error handling scan result", e);
}
}
};
}
return newLeScanCallback;
}
public void logMessageContent(byte[] value) {
if (value != null) {
for (byte b : value) {
LOG.warn("DATA: " + String.format("0x%2x", b) + " - " + (char) (b & 0xff));
}
}
}
private final Runnable stopRunnable = new Runnable() {
private Runnable stopRunnable = new Runnable() {
@Override
public void run() {
stopDiscovery();
@ -247,16 +94,15 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
private ProgressBar progressView;
private BluetoothAdapter adapter;
private final ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
private ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
private DeviceCandidateAdapter cadidateListAdapter;
private Button startButton;
private Scanning isScanning = Scanning.SCANNING_OFF;
private GBDeviceCandidate bondingDevice;
private String bondingAddress;
private enum Scanning {
SCANNING_BT,
SCANNING_BTLE,
SCANNING_NEW_BTLE,
SCANNING_OFF
}
@ -285,7 +131,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
IntentFilter bluetoothIntents = new IntentFilter();
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
bluetoothIntents.addAction(BluetoothDevice.ACTION_UUID);
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
@ -325,44 +170,13 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
@Override
protected void onDestroy() {
try {
unregisterReceiver(bluetoothReceiver);
} catch (IllegalArgumentException e) {
LOG.warn("Tried to unregister Bluetooth Receiver that wasn't registered.");
}
unregisterReceiver(bluetoothReceiver);
super.onDestroy();
}
private void handleDeviceFound(BluetoothDevice device, short rssi) {
ParcelUuid[] uuids = device.getUuids();
if (uuids == null) {
if (device.fetchUuidsWithSdp()) {
return;
}
}
handleDeviceFound(device, rssi, uuids);
}
private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
if (LOG.isDebugEnabled()) {
if (uuids != null && uuids.length > 0) {
for (ParcelUuid uuid : uuids) {
LOG.debug(" supports uuid: " + uuid.toString());
}
}
}
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
return; // ignore already bonded devices
}
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
if (deviceType.isSupported()) {
candidate.setDeviceType(deviceType);
LOG.info("Recognized supported device: " + candidate);
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi);
if (DeviceHelper.getInstance().isSupported(candidate)) {
int index = deviceCandidates.indexOf(candidate);
if (index >= 0) {
deviceCandidates.set(index, candidate); // replace
@ -397,16 +211,10 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
} else {
discoveryFinished();
}
} else if (what == Scanning.SCANNING_NEW_BTLE) {
if (GB.supportsBluetoothLE()) {
startNEWBTLEDiscovery();
} else {
discoveryFinished();
}
}
} else {
discoveryFinished();
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
Toast.makeText(this, "Enable Bluetooth to discover devices.", Toast.LENGTH_LONG).show();
}
}
@ -426,8 +234,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
stopBTDiscovery();
} else if (wasScanning == Scanning.SCANNING_BTLE) {
stopBTLEDiscovery();
} else if (wasScanning == Scanning.SCANNING_NEW_BTLE) {
stopNewBTLEDiscovery();
}
handler.removeMessages(0, stopRunnable);
}
@ -441,20 +247,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
adapter.cancelDiscovery();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void stopNewBTLEDiscovery() {
adapter.getBluetoothLeScanner().stopScan(newLeScanCallback);
}
private void bluetoothStateChanged(int oldState, int newState) {
discoveryFinished();
if (newState == BluetoothAdapter.STATE_ON) {
this.adapter = BluetoothAdapter.getDefaultAdapter();
startButton.setEnabled(true);
} else {
this.adapter = null;
startButton.setEnabled(false);
}
startButton.setEnabled(newState == BluetoothAdapter.STATE_ON);
}
private void discoveryFinished() {
@ -489,15 +284,8 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
return false;
}
BluetoothAdapter adapter = bluetoothService.getAdapter();
if (adapter == null) {
LOG.warn("No bluetooth available");
this.adapter = null;
return false;
}
if (!adapter.isEnabled()) {
LOG.warn("Bluetooth not enabled");
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(enableBtIntent);
this.adapter = null;
return false;
}
@ -505,41 +293,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
return true;
}
// New BTLE Discovery use startScan (List<ScanFilter> filters,
// ScanSettings settings,
// ScanCallback callback)
// It's added on API21
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startNEWBTLEDiscovery() {
// Only use new API when user uses Lollipop+ device
LOG.info("Start New BTLE Discovery");
handler.removeMessages(0, stopRunnable);
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
adapter.getBluetoothLeScanner().startScan(getScanFilters(), getScanSettings(), getScanCallback());
}
private List<ScanFilter> getScanFilters() {
List<ScanFilter> allFilters = new ArrayList<>();
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
allFilters.addAll(coordinator.createBLEScanFilters());
}
return allFilters;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ScanSettings getScanSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return new ScanSettings.Builder()
.setScanMode(SCAN_MODE_LOW_LATENCY)
.setMatchMode(MATCH_MODE_STICKY)
.build();
} else {
return new ScanSettings.Builder()
.setScanMode(SCAN_MODE_LOW_LATENCY)
.build();
}
}
private void startBTLEDiscovery() {
LOG.info("Starting BTLE Discovery");
handler.removeMessages(0, stopRunnable);
@ -554,12 +307,6 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
adapter.startDiscovery();
}
private void checkAndRequestLocationPermission() {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0);
}
}
private Message getPostMessage(Runnable runnable) {
Message m = Message.obtain(handler, runnable);
m.obj = runnable;
@ -576,35 +323,17 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
stopDiscovery();
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass());
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
Intent intent = new Intent(this, pairingActivity);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS, deviceCandidate.getMacAddress());
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: {
createBond(deviceCandidate, bondingStyle);
break;
}
case BluetoothDevice.BOND_BONDING:
// async, wait for bonding event to finish this activity
bondingDevice = deviceCandidate;
break;
case BluetoothDevice.BOND_BONDED:
handleDeviceBonded();
break;
if (btDevice.createBond()) {
// async, wait for bonding event to finish this activity
bondingAddress = btDevice.getAddress();
}
} catch (Exception e) {
LOG.error("Error pairing device: " + deviceCandidate.getMacAddress());

View File

@ -1,392 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo, Uwe Hermann
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.util.Log;
import android.view.MenuItem;
import android.webkit.ConsoleMessage;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Scanner;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class ExternalPebbleJSActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
private UUID appUuid;
private Uri confUri;
private GBDevice mGBDevice = null;
private WebView myWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
appUuid = (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
setContentView(R.layout.activity_external_pebble_js);
myWebView = (WebView) findViewById(R.id.configureWebview);
myWebView.clearCache(true);
myWebView.setWebViewClient(new GBWebClient());
myWebView.setWebChromeClient(new GBChromeClient());
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
//needed to access the DOM
webSettings.setDomStorageEnabled(true);
//needed for localstorage
webSettings.setDatabaseEnabled(true);
JSInterface gbJSInterface = new JSInterface(this);
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
myWebView.loadUrl("file:///android_asset/app_config/configure.html");
}
@Override
protected void onNewIntent(Intent incoming) {
incoming.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
super.onNewIntent(incoming);
confUri = incoming.getData();
}
@Override
protected void onResume() {
super.onResume();
String queryString = "";
if (confUri != null) {
//getting back with configuration data
try {
appUuid = UUID.fromString(confUri.getHost());
queryString = confUri.getEncodedQuery();
} catch (IllegalArgumentException e) {
GB.toast("returned uri: " + confUri.toString(), Toast.LENGTH_LONG, GB.ERROR);
}
myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
}
}
private JSONObject getAppConfigurationKeys() {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, appUuid.toString() + ".json");
if (configurationFile.exists()) {
String jsonstring = FileUtils.getStringFromFile(configurationFile);
JSONObject json = new JSONObject(jsonstring);
return json.getJSONObject("appKeys");
}
} catch (IOException | JSONException e) {
e.printStackTrace();
}
return null;
}
private boolean isLocationEnabledForWatchApp() {
return true; //as long as we don't give watchapp internet access it's not a problem
}
private class GBChromeClient extends WebChromeClient {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
GB.toast(consoleMessage.message(), Toast.LENGTH_LONG, GB.ERROR);
//TODO: show error page
}
return super.onConsoleMessage(consoleMessage);
}
}
private class GBWebClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("http://") || url.startsWith("https://")) {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
} else {
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
view.loadUrl(url);
}
return true;
}
}
private class JSInterface {
Context mContext;
public JSInterface(Context c) {
mContext = c;
}
@JavascriptInterface
public void gbLog(String msg) {
Log.d("WEBVIEW", msg);
}
@JavascriptInterface
public void sendAppMessage(String msg) {
LOG.debug("from WEBVIEW: " + msg);
JSONObject knownKeys = getAppConfigurationKeys();
try {
JSONObject in = new JSONObject(msg);
JSONObject out = new JSONObject();
String inKey, outKey;
boolean passKey;
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
passKey = false;
inKey = key.next();
outKey = null;
int pebbleAppIndex = knownKeys.optInt(inKey, -1);
if (pebbleAppIndex != -1) {
passKey = true;
outKey = String.valueOf(pebbleAppIndex);
} else {
//do not discard integer keys (see https://developer.pebble.com/guides/communication/using-pebblekit-js/ )
Scanner scanner = new Scanner(inKey);
if (scanner.hasNextInt() && inKey.equals("" + scanner.nextInt())) {
passKey = true;
outKey = inKey;
}
}
if (passKey) {
Object obj = in.get(inKey);
out.put(outKey, obj);
} else {
GB.toast("Discarded key " + inKey + ", not found in the local configuration and is not an integer key.", Toast.LENGTH_SHORT, GB.WARN);
}
}
LOG.info(out.toString());
GBApplication.deviceService().onAppConfiguration(appUuid, out.toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
@JavascriptInterface
public String getActiveWatchInfo() {
JSONObject wi = new JSONObject();
try {
wi.put("firmware", mGBDevice.getFirmwareVersion());
wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getModel()));
wi.put("model", PebbleUtils.getModel(mGBDevice.getModel()));
//TODO: use real info
wi.put("language", "en");
} catch (JSONException e) {
e.printStackTrace();
}
//Json not supported apparently, we need to cast back and forth
return wi.toString();
}
@JavascriptInterface
public String getAppConfigurationFile() {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, appUuid.toString() + "_config.js");
if (configurationFile.exists()) {
return "file:///" + configurationFile.getAbsolutePath();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@JavascriptInterface
public String getAppStoredPreset() {
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File configurationFile = new File(destDir, appUuid.toString() + "_preset.json");
if (configurationFile.exists()) {
return FileUtils.getStringFromFile(configurationFile);
}
} catch (IOException e) {
GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
e.printStackTrace();
}
return null;
}
@JavascriptInterface
public void saveAppStoredPreset(String msg) {
Writer writer;
try {
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
File presetsFile = new File(destDir, appUuid.toString() + "_preset.json");
writer = new BufferedWriter(new FileWriter(presetsFile));
writer.write(msg);
writer.close();
GB.toast("Presets stored", Toast.LENGTH_SHORT, GB.INFO);
} catch (IOException e) {
GB.toast("Error storing presets", Toast.LENGTH_LONG, GB.ERROR);
e.printStackTrace();
}
}
@JavascriptInterface
public String getAppUUID() {
return appUuid.toString();
}
@JavascriptInterface
public String getAppLocalstoragePrefix() {
String prefix = mGBDevice.getAddress() + appUuid.toString();
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] bytes = prefix.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
bytes = digest.digest();
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
sb.append(String.format("%02X", bytes[i]));
}
return sb.toString().toLowerCase();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
return prefix;
}
}
@JavascriptInterface
public String getWatchToken() {
//specification says: A string that is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
return "gb" + appUuid.toString();
}
@JavascriptInterface
public void closeActivity() {
NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
}
@JavascriptInterface
public String getCurrentPosition() {
if (!isLocationEnabledForWatchApp()) {
return "";
}
//we need to override this because the coarse location is not enough for the android webview, we should add the permission for fine location.
JSONObject geoPosition = new JSONObject();
JSONObject coords = new JSONObject();
try {
Prefs prefs = GBApplication.getPrefs();
geoPosition.put("timestamp", (System.currentTimeMillis() / 1000) - 86400); //let JS know this value is really old
coords.put("latitude", prefs.getFloat("location_latitude", 0));
coords.put("longitude", prefs.getFloat("location_longitude", 0));
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
prefs.getBoolean("use_updated_location_if_available", false)) {
LocationManager locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
if (lastKnownLocation != null) {
geoPosition.put("timestamp", lastKnownLocation.getTime());
coords.put("latitude", (float) lastKnownLocation.getLatitude());
coords.put("longitude", (float) lastKnownLocation.getLongitude());
coords.put("accuracy", lastKnownLocation.getAccuracy());
coords.put("altitude", lastKnownLocation.getAltitude());
coords.put("speed", lastKnownLocation.getSpeed());
}
}
}
geoPosition.put("coords", coords);
} catch (JSONException e) {
e.printStackTrace();
}
return geoPosition.toString();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@ -1,22 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -28,6 +12,7 @@ import android.support.v4.content.LocalBroadcastManager;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -45,16 +30,14 @@ import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FwAppInstallerActivity extends AbstractGBActivity implements InstallActivity {
public class FwAppInstallerActivity extends Activity implements InstallActivity {
private static final Logger LOG = LoggerFactory.getLogger(FwAppInstallerActivity.class);
private static final String ITEM_DETAILS = "details";
private TextView fwAppInstallTextView;
private Button installButton;
@ -63,20 +46,13 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
private InstallHandler installHandler;
private boolean mayConnect;
private ProgressBar mProgressBar;
private ListView itemListView;
private final List<ItemWithDetails> mItems = new ArrayList<>();
private ItemWithDetailsAdapter mItemAdapter;
private ListView detailsListView;
private ItemWithDetailsAdapter mDetailsItemAdapter;
private ArrayList<ItemWithDetails> mDetails = new ArrayList<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
if (action.equals(ControlCenter.ACTION_QUIT)) {
finish();
} else if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null) {
refreshBusyState(device);
@ -92,13 +68,13 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
validateInstallation();
}
}
} else if (GB.ACTION_DISPLAY_MESSAGE.equals(action)) {
String message = intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE);
int severity = intent.getIntExtra(GB.DISPLAY_MESSAGE_SEVERITY, GB.INFO);
addMessage(message, severity);
}
}
};
private ProgressBar mProgressBar;
private ListView itemListView;
private List<ItemWithDetails> mItems = new ArrayList<>();
private ItemWithDetailsAdapter mItemAdapter;
private void refreshBusyState(GBDevice dev) {
if (dev.isConnecting() || dev.isBusy()) {
@ -114,7 +90,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
private void connect() {
mayConnect = false; // only do that once per #onCreate
GBApplication.deviceService().connect(device);
GBApplication.deviceService().connect(device != null ? device.getAddress() : null);
}
private void validateInstallation() {
@ -127,18 +103,11 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_appinstaller);
getActionBar().setDisplayHomeAsUpEnabled(true);
GBDevice dev = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null) {
device = dev;
}
if (savedInstanceState != null) {
mDetails = savedInstanceState.getParcelableArrayList(ITEM_DETAILS);
if (mDetails == null) {
mDetails = new ArrayList<>();
}
}
mayConnect = true;
itemListView = (ListView) findViewById(R.id.itemListView);
mItemAdapter = new ItemWithDetailsAdapter(this, mItems);
@ -146,14 +115,10 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
fwAppInstallTextView = (TextView) findViewById(R.id.infoTextView);
installButton = (Button) findViewById(R.id.installButton);
mProgressBar = (ProgressBar) findViewById(R.id.installProgressBar);
detailsListView = (ListView) findViewById(R.id.detailsListView);
mDetailsItemAdapter = new ItemWithDetailsAdapter(this, mDetails);
mDetailsItemAdapter.setSize(ItemWithDetailsAdapter.SIZE_SMALL);
detailsListView.setAdapter(mDetailsItemAdapter);
setInstallEnabled(false);
IntentFilter filter = new IntentFilter();
filter.addAction(ControlCenter.ACTION_QUIT);
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filter.addAction(GB.ACTION_DISPLAY_MESSAGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
installButton.setOnClickListener(new View.OnClickListener() {
@ -166,9 +131,6 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
});
uri = getIntent().getData();
if (uri == null) { //for "share" intent
uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
installHandler = findInstallHandlerFor(uri);
if (installHandler == null) {
setInfoText(getString(R.string.installer_activity_unable_to_find_handler));
@ -184,12 +146,6 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(ITEM_DETAILS, mDetails);
}
private InstallHandler findInstallHandlerFor(Uri uri) {
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
InstallHandler handler = coordinator.findInstallHandler(uri, this);
@ -221,11 +177,6 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
fwAppInstallTextView.setText(text);
}
@Override
public CharSequence getInfoText() {
return fwAppInstallTextView.getText();
}
@Override
public void setInstallEnabled(boolean enable) {
boolean enabled = device != null && device.isConnected() && enable;
@ -245,9 +196,4 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
mItems.add(item);
mItemAdapter.notifyDataSetChanged();
}
private void addMessage(String message, int severity) {
mDetails.add(new GenericItem(message));
mDetailsItemAdapter.notifyDataSetChanged();
}
}

View File

@ -1,25 +0,0 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import java.util.Locale;
public interface GBActivity {
void setLanguage(Locale language, boolean invalidateLanguage);
void setTheme(int themeId);
}

View File

@ -1,30 +0,0 @@
/* Copyright (C) 2016-2017 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.activities;
public class HeartRateUtils {
public static final int MAX_HEART_RATE_VALUE = 250;
public static final int MIN_HEART_RATE_VALUE = 0;
/**
* The maxiumum gap between two hr measurements in which
* we interpolate between the measurements. Otherwise, two
* distinct measurements will be shown.
*
* Value is in minutes
*/
public static final int MAX_HR_MEASUREMENTS_GAP_MINUTES = 10;
}

View File

@ -1,32 +1,12 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
public interface InstallActivity {
CharSequence getInfoText();
void setInfoText(String text);
void setInstallEnabled(boolean enable);
void clearInstallItems();
void setInstallItem(ItemWithDetails item);
}

View File

@ -1,68 +1,14 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Normano64
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_STEPS_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_WEIGHT_KG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_YEAR_OF_BIRTH;
public class SettingsActivity extends AbstractSettingsActivity {
private static final Logger LOG = LoggerFactory.getLogger(SettingsActivity.class);
public static final String PREF_MEASUREMENT_SYSTEM = "measurement_system";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -99,20 +45,11 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
pref = findPreference("pref_key_blacklist_calendars");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, CalBlacklistActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pebble_emu_addr");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
final Preference pebbleEmuAddr = findPreference("pebble_emu_addr");
pebbleEmuAddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
@ -120,237 +57,25 @@ public class SettingsActivity extends AbstractSettingsActivity {
});
pref = findPreference("pebble_emu_port");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
final Preference pebbleEmuPort = findPreference("pebble_emu_port");
pebbleEmuPort.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
preference.setSummary(newVal.toString());
return true;
}
});
pref = findPreference("log_to_file");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
boolean doEnable = Boolean.TRUE.equals(newVal);
try {
if (doEnable) {
FileUtils.getExternalFilesDir(); // ensures that it is created
}
GBApplication.setupLogging(doEnable);
} catch (IOException ex) {
GB.toast(getApplicationContext(),
getString(R.string.error_creating_directory_for_logfiles, ex.getLocalizedMessage()),
Toast.LENGTH_LONG,
GB.ERROR,
ex);
}
return true;
}
});
pref = findPreference("language");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
String newLang = newVal.toString();
try {
GBApplication.setLanguage(newLang);
recreate();
} catch (Exception ex) {
GB.toast(getApplicationContext(),
"Error setting language: " + ex.getLocalizedMessage(),
Toast.LENGTH_LONG,
GB.ERROR,
ex);
}
return true;
}
});
final Preference unit = findPreference(PREF_MEASUREMENT_SYSTEM);
unit.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MEASUREMENT_SYSTEM);
}
});
preference.setSummary(newVal.toString());
return true;
}
});
if (!GBApplication.isRunningMarshmallowOrLater()) {
pref = findPreference("notification_filter");
PreferenceCategory category = (PreferenceCategory) findPreference("pref_key_notifications");
category.removePreference(pref);
}
pref = findPreference("location_aquire");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(SettingsActivity.this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0);
}
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, false);
if (provider != null) {
Location location = locationManager.getLastKnownLocation(provider);
if (location != null) {
setLocationPreferences(location);
} else {
locationManager.requestSingleUpdate(provider, new LocationListener() {
@Override
public void onLocationChanged(Location location) {
setLocationPreferences(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
LOG.info("provider status changed to " + status + " (" + provider + ")");
}
@Override
public void onProviderEnabled(String provider) {
LOG.info("provider enabled (" + provider + ")");
}
@Override
public void onProviderDisabled(String provider) {
LOG.info("provider disabled (" + provider + ")");
GB.toast(SettingsActivity.this, getString(R.string.toast_enable_networklocationprovider), 3000, 0);
}
}, null);
}
} else {
LOG.warn("No location provider found, did you deny location permission?");
}
return true;
}
});
pref = findPreference("canned_messages_dismisscall_send");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Prefs prefs = GBApplication.getPrefs();
ArrayList<String> messages = new ArrayList<>();
for (int i = 1; i <= 16; i++) {
String message = prefs.getString("canned_message_dismisscall_" + i, null);
if (message != null && !message.equals("")) {
messages.add(message);
}
}
CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec();
cannedMessagesSpec.type = CannedMessagesSpec.TYPE_MISSEDCALLS;
cannedMessagesSpec.cannedMessages = messages.toArray(new String[messages.size()]);
GBApplication.deviceService().onSetCannedMessages(cannedMessagesSpec);
return true;
}
});
// Get all receivers of Media Buttons
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
PackageManager pm = getPackageManager();
List<ResolveInfo> mediaReceivers = pm.queryBroadcastReceivers(mediaButtonIntent,
PackageManager.GET_INTENT_FILTERS | PackageManager.GET_RESOLVED_FILTER);
CharSequence[] newEntries = new CharSequence[mediaReceivers.size() + 1];
CharSequence[] newValues = new CharSequence[mediaReceivers.size() + 1];
newEntries[0] = getString(R.string.pref_default);
newValues[0] = "default";
int i = 1;
for (ResolveInfo resolveInfo : mediaReceivers) {
newEntries[i] = resolveInfo.activityInfo.loadLabel(pm);
newValues[i] = resolveInfo.activityInfo.packageName;
i++;
}
final ListPreference audioPlayer = (ListPreference) findPreference("audio_player");
audioPlayer.setEntries(newEntries);
audioPlayer.setEntryValues(newValues);
audioPlayer.setDefaultValue(newValues[0]);
}
/*
* delayed execution so that the preferences are applied first
*/
private void invokeLater(Runnable runnable) {
getListView().post(runnable);
}
@Override
protected String[] getPreferenceKeysWithSummary() {
return new String[]{
"pebble_emu_addr",
"pebble_emu_port",
"pebble_reconnect_attempts",
"location_latitude",
"location_longitude",
"canned_reply_suffix",
"canned_reply_1",
"canned_reply_2",
"canned_reply_3",
"canned_reply_4",
"canned_reply_5",
"canned_reply_6",
"canned_reply_7",
"canned_reply_8",
"canned_reply_9",
"canned_reply_10",
"canned_reply_11",
"canned_reply_12",
"canned_reply_13",
"canned_reply_14",
"canned_reply_15",
"canned_reply_16",
"canned_message_dismisscall_1",
"canned_message_dismisscall_2",
"canned_message_dismisscall_3",
"canned_message_dismisscall_4",
"canned_message_dismisscall_5",
"canned_message_dismisscall_6",
"canned_message_dismisscall_7",
"canned_message_dismisscall_8",
"canned_message_dismisscall_9",
"canned_message_dismisscall_10",
"canned_message_dismisscall_11",
"canned_message_dismisscall_12",
"canned_message_dismisscall_13",
"canned_message_dismisscall_14",
"canned_message_dismisscall_15",
"canned_message_dismisscall_16",
PREF_USER_YEAR_OF_BIRTH,
PREF_USER_HEIGHT_CM,
PREF_USER_WEIGHT_KG,
PREF_USER_SLEEP_DURATION,
PREF_USER_STEPS_GOAL,
"pebble_emu_port"
};
}
private void setLocationPreferences(Location location) {
String latitude = String.format(Locale.US, "%.6g", location.getLatitude());
String longitude = String.format(Locale.US, "%.6g", location.getLongitude());
LOG.info("got location. Lat: " + latitude + " Lng: " + longitude);
GB.toast(SettingsActivity.this, getString(R.string.toast_aqurired_networklocation), 2000, 0);
EditTextPreference pref_latitude = (EditTextPreference) findPreference("location_latitude");
EditTextPreference pref_longitude = (EditTextPreference) findPreference("location_longitude");
pref_latitude.setText(latitude);
pref_longitude.setText(longitude);
pref_latitude.setSummary(latitude);
pref_longitude.setSummary(longitude);
}
}

View File

@ -1,59 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import android.widget.SeekBar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
public class VibrationActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(VibrationActivity.class);
private SeekBar seekBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_vibration);
seekBar = (SeekBar) findViewById(R.id.vibration_seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (progress > 0) { // 1-16
progress = progress * 16 - 1; // max 255
}
GBApplication.deviceService().onSetConstantVibration(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
}

View File

@ -1,487 +0,0 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
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;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
public abstract class AbstractAppManagerFragment extends Fragment {
public static final String ACTION_REFRESH_APPLIST
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
private ItemTouchHelper appManagementTouchHelper;
protected abstract List<GBDeviceApp> getSystemAppsInCategory();
protected abstract String getSortFilename();
protected abstract boolean isCacheManager();
protected abstract boolean filterApp(GBDeviceApp gbDeviceApp);
public void startDragging(RecyclerView.ViewHolder viewHolder) {
appManagementTouchHelper.startDrag(viewHolder);
}
protected void onChangedAppOrder() {
List<UUID> uuidList = new ArrayList<>();
for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getAppList()) {
uuidList.add(gbDeviceApp.getUUID());
}
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuidList);
}
protected void refreshList() {
appList.clear();
ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename());
List<GBDeviceApp> systemApps = getSystemAppsInCategory();
boolean needsRewrite = false;
for (GBDeviceApp systemApp : systemApps) {
if (!uuids.contains(systemApp.getUUID())) {
uuids.add(systemApp.getUUID());
needsRewrite = true;
}
}
if (needsRewrite) {
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids);
}
appList.addAll(getCachedApps(uuids));
}
private void refreshListFromPebble(Intent intent) {
appList.clear();
int appCount = intent.getIntExtra("app_count", 0);
for (Integer i = 0; i < appCount; i++) {
String appName = intent.getStringExtra("app_name" + i.toString());
String appCreator = intent.getStringExtra("app_creator" + i.toString());
UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, "", appType);
app.setOnDevice(true);
if (filterApp(app)) {
appList.add(app);
}
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(ACTION_REFRESH_APPLIST)) {
if (intent.hasExtra("app_count")) {
LOG.info("got app info from pebble");
if (!isCacheManager()) {
LOG.info("will refresh list based on data from pebble");
refreshListFromPebble(intent);
}
} else if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3 || isCacheManager()) {
refreshList();
}
mGBDeviceAppAdapter.notifyDataSetChanged();
}
}
};
protected final List<GBDeviceApp> appList = new ArrayList<>();
private GBDeviceAppAdapter mGBDeviceAppAdapter;
protected GBDevice mGBDevice = null;
protected List<GBDeviceApp> getCachedApps(List<UUID> uuids) {
List<GBDeviceApp> cachedAppList = new ArrayList<>();
File cachePath;
try {
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
} catch (IOException e) {
LOG.warn("could not get external dir while reading pbw cache.");
return cachedAppList;
}
File[] files;
if (uuids == null) {
files = cachePath.listFiles();
} else {
files = new File[uuids.size()];
int index = 0;
for (UUID uuid : uuids) {
files[index++] = new File(uuid.toString() + ".pbw");
}
}
if (files != null) {
for (File file : files) {
if (file.getName().endsWith(".pbw")) {
String baseName = file.getName().substring(0, file.getName().length() - 4);
//metadata
File jsonFile = new File(cachePath, baseName + ".json");
//configuration
File configFile = new File(cachePath, baseName + "_config.js");
try {
String jsonstring = FileUtils.getStringFromFile(jsonFile);
JSONObject json = new JSONObject(jsonstring);
cachedAppList.add(new GBDeviceApp(json, configFile.exists()));
} catch (Exception e) {
LOG.info("could not read json file for " + baseName);
//FIXME: this is really ugly, if we do not find system uuids in pbw cache add them manually. Also duplicated code
switch (baseName) {
case "8f3c8686-31a1-4f5f-91f5-01600c9bdc59":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
break;
case "1f03293d-47af-4f28-b960-f2b02a6dd757":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "b2cae818-10f8-46df-ad2b-98ad2254a3c1":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "67a32d95-ef69-46d4-a0b9-854cc62f97f9":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "18e443ce-38fd-47c8-84d5-6d0c775fbe55":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
case "0863fc6a-66c5-4f62-ab8a-82ed00a98b5d":
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
break;
}
/*
else if (baseName.equals("4dab81a6-d2fc-458a-992c-7a1f3b96a970")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
} else if (baseName.equals("cf1e816a-9db0-4511-bbb8-f60c48ca8fac")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
*/
if (mGBDevice != null) {
if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
if (baseName.equals(PebbleProtocol.UUID_PEBBLE_HEALTH.toString())) {
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
continue;
}
}
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
if (baseName.equals(PebbleProtocol.UUID_WORKOUT.toString())) {
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
continue;
}
}
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
if (baseName.equals("3af858c3-16cb-4561-91e7-f1ad2df8725f")) {
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
}
if (baseName.equals(PebbleProtocol.UUID_WEATHER.toString())) {
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
}
}
if (uuids == null) {
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN));
}
}
}
}
}
return cachedAppList;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_REFRESH_APPLIST);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, filter);
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3) {
GBApplication.deviceService().onAppInfoReq();
if (isCacheManager()) {
refreshList();
}
} else {
refreshList();
}
}
@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_pebble_watchapp, this);
appListView.setAdapter(mGBDeviceAppAdapter);
ItemTouchHelper.Callback appItemTouchHelperCallback = new AppItemTouchHelperCallback(mGBDeviceAppAdapter);
appManagementTouchHelper = new ItemTouchHelper(appItemTouchHelperCallback);
appManagementTouchHelper.attachToRecyclerView(appListView);
return rootView;
}
protected void sendOrderToDevice(String concatFilename) {
ArrayList<UUID> uuids = new ArrayList<>();
for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getAppList()) {
uuids.add(gbDeviceApp.getUUID());
}
if (concatFilename != null) {
ArrayList<UUID> concatUuids = AppManagerActivity.getUuidsFromFile(concatFilename);
uuids.addAll(concatUuids);
}
GBApplication.deviceService().onAppReorder(uuids.toArray(new UUID[uuids.size()]));
}
public boolean openPopupMenu(View view, GBDeviceApp deviceApp) {
PopupMenu popupMenu = new PopupMenu(getContext(), view);
popupMenu.getMenuInflater().inflate(R.menu.appmanager_context, popupMenu.getMenu());
Menu menu = popupMenu.getMenu();
final GBDeviceApp selectedApp = deviceApp;
if (!selectedApp.isInCache()) {
menu.removeItem(R.id.appmanager_app_reinstall);
menu.removeItem(R.id.appmanager_app_delete_cache);
}
if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_health_activate);
menu.removeItem(R.id.appmanager_health_deactivate);
}
if (!PebbleProtocol.UUID_WORKOUT.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_hrm_activate);
menu.removeItem(R.id.appmanager_hrm_deactivate);
}
if (!PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
menu.removeItem(R.id.appmanager_weather_activate);
menu.removeItem(R.id.appmanager_weather_deactivate);
menu.removeItem(R.id.appmanager_weather_install_provider);
}
if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) {
menu.removeItem(R.id.appmanager_app_delete);
}
if (!selectedApp.isConfigurable()) {
menu.removeItem(R.id.appmanager_app_configure);
}
if (PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
PackageManager pm = getActivity().getPackageManager();
try {
pm.getPackageInfo("ru.gelin.android.weather.notification", PackageManager.GET_ACTIVITIES);
menu.removeItem(R.id.appmanager_weather_install_provider);
} catch (PackageManager.NameNotFoundException e) {
menu.removeItem(R.id.appmanager_weather_activate);
menu.removeItem(R.id.appmanager_weather_deactivate);
}
}
switch (selectedApp.getType()) {
case WATCHFACE:
case APP_GENERIC:
case APP_ACTIVITYTRACKER:
break;
default:
menu.removeItem(R.id.appmanager_app_openinstore);
}
//menu.setHeaderTitle(selectedApp.getName());
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
return onContextItemSelected(item, selectedApp);
}
}
);
popupMenu.show();
return true;
}
private boolean onContextItemSelected(MenuItem item, GBDeviceApp selectedApp) {
switch (item.getItemId()) {
case R.id.appmanager_app_delete_cache:
String baseName;
try {
baseName = FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID();
} catch (IOException e) {
LOG.warn("could not get external dir while trying to access pbw cache.");
return true;
}
String[] suffixToDelete = new String[]{".pbw", ".json", "_config.js", "_preset.json"};
for (String suffix : suffixToDelete) {
File fileToDelete = new File(baseName + suffix);
if (!fileToDelete.delete()) {
LOG.warn("could not delete file from pbw cache: " + fileToDelete.toString());
} else {
LOG.info("deleted file: " + fileToDelete.toString());
}
}
AppManagerActivity.deleteFromAppOrderFile("pbwcacheorder.txt", selectedApp.getUUID()); // FIXME: only if successful
// fall through
case R.id.appmanager_app_delete:
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3) {
AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchapps", selectedApp.getUUID()); // FIXME: only if successful
AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchfaces", selectedApp.getUUID()); // FIXME: only if successful
Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(refreshIntent);
}
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_app_reinstall:
File cachePath;
try {
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw");
} catch (IOException e) {
LOG.warn("could not get external dir while trying to access pbw cache.");
return true;
}
GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath));
return true;
case R.id.appmanager_health_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
return true;
case R.id.appmanager_hrm_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://hrm"));
return true;
case R.id.appmanager_weather_activate:
GBApplication.deviceService().onInstallApp(Uri.parse("fake://weather"));
return true;
case R.id.appmanager_health_deactivate:
case R.id.appmanager_hrm_deactivate:
case R.id.appmanager_weather_deactivate:
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
return true;
case R.id.appmanager_weather_install_provider:
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/app/ru.gelin.android.weather.notification")));
return true;
case R.id.appmanager_app_configure:
GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);
Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class);
startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID());
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
startActivity(startIntent);
return true;
case R.id.appmanager_app_openinstore:
String url = "https://apps.getpebble.com/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1?query=" + selectedApp.getName() + "&dev_settings=true";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
startActivity(intent);
return true;
default:
return super.onContextItemSelected(item);
}
}
@Override
public void onDestroy() {
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
super.onDestroy();
}
public class AppItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final GBDeviceAppAdapter gbDeviceAppAdapter;
public AppItemTouchHelperCallback(GBDeviceAppAdapter gbDeviceAppAdapter) {
this.gbDeviceAppAdapter = gbDeviceAppAdapter;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//app reordering is not possible on old firmwares
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3 && !isCacheManager()) {
return 0;
}
//we only support up and down movement and only for moving, not for swiping apps away
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
gbDeviceAppAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
//nothing to do
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
onChangedAppOrder();
}
}
}

View File

@ -1,198 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, 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.activities.appmanager;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v4.view.ViewPager;
import android.view.MenuItem;
import android.view.View;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class AppManagerActivity extends AbstractGBFragmentActivity {
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
private int READ_REQUEST_CODE = 42;
private GBDevice mGBDevice = null;
public GBDevice getGBDevice() {
return mGBDevice;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragmentappmanager);
Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
assert fab != null;
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
});
// Set up the ViewPager with the sections adapter.
ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
if (viewPager != null) {
viewPager.setAdapter(getPagerAdapter());
}
}
@Override
protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager) {
return new SectionsPagerAdapter(fragmentManager);
}
public static synchronized void deleteFromAppOrderFile(String filename, UUID uuid) {
ArrayList<UUID> uuids = getUuidsFromFile(filename);
uuids.remove(uuid);
rewriteAppOrderFile(filename, uuids);
}
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
switch (position) {
case 0:
return new AppManagerFragmentCache();
case 1:
return new AppManagerFragmentInstalledApps();
case 2:
return new AppManagerFragmentInstalledWatchfaces();
}
return null;
}
@Override
public int getCount() {
return 3;
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.appmanager_cached_watchapps_watchfaces);
case 1:
return getString(R.string.appmanager_installed_watchapps);
case 2:
return getString(R.string.appmanager_installed_watchfaces);
case 3:
}
return super.getPageTitle(position);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
static synchronized void rewriteAppOrderFile(String filename, List<UUID> uuids) {
try (BufferedWriter out = new BufferedWriter(new FileWriter(FileUtils.getExternalFilesDir() + "/" + filename))) {
for (UUID uuid : uuids) {
out.write(uuid.toString());
out.newLine();
}
} catch (IOException e) {
LOG.warn("can't write app order to file!");
}
}
synchronized public static void addToAppOrderFile(String filename, UUID uuid) {
ArrayList<UUID> uuids = getUuidsFromFile(filename);
if (!uuids.contains(uuid)) {
uuids.add(uuid);
rewriteAppOrderFile(filename, uuids);
}
}
static synchronized ArrayList<UUID> getUuidsFromFile(String filename) {
ArrayList<UUID> uuids = new ArrayList<>();
try (BufferedReader in = new BufferedReader(new FileReader(FileUtils.getExternalFilesDir() + "/" + filename))) {
String line;
while ((line = in.readLine()) != null) {
uuids.add(UUID.fromString(line));
}
} catch (IOException e) {
LOG.warn("could not read sort file");
}
return uuids;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Intent startIntent = new Intent(AppManagerActivity.this, FwAppInstallerActivity.class);
startIntent.setAction(Intent.ACTION_VIEW);
startIntent.setDataAndType(resultData.getData(), null);
startActivity(startIntent);
}
}
}

View File

@ -1,49 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
public class AppManagerFragmentCache extends AbstractAppManagerFragment {
@Override
public void refreshList() {
appList.clear();
appList.addAll(getCachedApps(null));
}
@Override
protected boolean isCacheManager() {
return true;
}
@Override
protected List<GBDeviceApp> getSystemAppsInCategory() {
return null;
}
@Override
public String getSortFilename() {
return "pbwcacheorder.txt";
}
@Override
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
return true;
}
}

View File

@ -1,75 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment {
@Override
protected List<GBDeviceApp> getSystemAppsInCategory() {
List<GBDeviceApp> systemApps = new ArrayList<>();
//systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
//systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("1f03293d-47af-4f28-b960-f2b02a6dd757"), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_NOTIFICATIONS, "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(UUID.fromString("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
if (mGBDevice != null) {
if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
systemApps.add(new GBDeviceApp(UUID.fromString("0863fc6a-66c5-4f62-ab8a-82ed00a98b5d"), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
}
}
return systemApps;
}
@Override
protected boolean isCacheManager() {
return false;
}
@Override
protected String getSortFilename() {
return mGBDevice.getAddress() + ".watchapps";
}
@Override
protected void onChangedAppOrder() {
super.onChangedAppOrder();
sendOrderToDevice(mGBDevice.getAddress() + ".watchfaces");
}
@Override
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
return gbDeviceApp.getType() == GBDeviceApp.Type.APP_ACTIVITYTRACKER || gbDeviceApp.getType() == GBDeviceApp.Type.APP_GENERIC;
}
}

View File

@ -1,58 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
public class AppManagerFragmentInstalledWatchfaces extends AbstractAppManagerFragment {
@Override
protected List<GBDeviceApp> getSystemAppsInCategory() {
List<GBDeviceApp> systemWatchfaces = new ArrayList<>();
systemWatchfaces.add(new GBDeviceApp(UUID.fromString("8f3c8686-31a1-4f5f-91f5-01600c9bdc59"), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
systemWatchfaces.add(new GBDeviceApp(UUID.fromString("3af858c3-16cb-4561-91e7-f1ad2df8725f"), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
return systemWatchfaces;
}
@Override
protected boolean isCacheManager() {
return false;
}
@Override
protected String getSortFilename() {
return mGBDevice.getAddress() + ".watchfaces";
}
@Override
protected void onChangedAppOrder() {
super.onChangedAppOrder();
sendOrderToDevice(mGBDevice.getAddress() + ".watchapps");
}
@Override
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
if (gbDeviceApp.getType() == GBDeviceApp.Type.WATCHFACE) {
return true;
}
return false;
}
}

View File

@ -1,50 +1,19 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, walkjivefly
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.TypedValue;
import android.view.View;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.CombinedData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -59,15 +28,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -93,36 +59,29 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
* shift the date by one day.
*/
public abstract class AbstractChartFragment extends AbstractGBFragment {
protected final int ANIM_TIME = 250;
protected int ANIM_TIME = 350;
private static final Logger LOG = LoggerFactory.getLogger(AbstractChartFragment.class);
private final Set<String> mIntentFilterActions;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
AbstractChartFragment.this.onReceive(context, intent);
}
};
private boolean mChartDirty = true;
private AsyncTask refreshTask;
public boolean isChartDirty() {
return mChartDirty;
}
@Override
public abstract String getTitle();
public boolean supportsHeartrate(GBDevice device) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator != null && coordinator.supportsHeartRateMeasurement(device);
}
protected static final class ActivityConfig {
public final int type;
public final String label;
public final Integer color;
public Integer color;
public ActivityConfig(int kind, String label, Integer color) {
this.type = kind;
@ -131,40 +90,36 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
protected ActivityConfig akActivity;
protected ActivityConfig akLightSleep;
protected ActivityConfig akDeepSleep;
protected ActivityConfig akNotWorn;
protected ActivityConfig akActivity = new ActivityConfig(ActivityKind.TYPE_ACTIVITY, "Activity", Color.rgb(89, 178, 44));
protected ActivityConfig akLightSleep = new ActivityConfig(ActivityKind.TYPE_LIGHT_SLEEP, "Light Sleep", Color.rgb(182, 191, 255));
protected ActivityConfig akDeepSleep = new ActivityConfig(ActivityKind.TYPE_DEEP_SLEEP, "Deep Sleep", Color.rgb(76, 90, 255));
protected ActivityConfig akNotWorn = new ActivityConfig(ActivityKind.TYPE_NOT_WORN, "Not Worn", Color.rgb(84, 82, 84));
protected int BACKGROUND_COLOR;
protected int DESCRIPTION_COLOR;
protected int CHART_TEXT_COLOR;
protected int LEGEND_TEXT_COLOR;
protected int HEARTRATE_COLOR;
protected int HEARTRATE_FILL_COLOR;
protected int AK_ACTIVITY_COLOR;
protected int AK_DEEP_SLEEP_COLOR;
protected int AK_LIGHT_SLEEP_COLOR;
protected int AK_NOT_WORN_COLOR;
protected String HEARTRATE_LABEL;
protected AbstractChartFragment(String... intentFilterActions) {
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
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
init();
initColors();
IntentFilter filter = new IntentFilter();
for (String action : mIntentFilterActions) {
@ -173,49 +128,36 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filter);
}
protected void init() {
TypedValue runningColor = new TypedValue();
BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
CHART_TEXT_COLOR = ContextCompat.getColor(getContext(), R.color.secondarytext);
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
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_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;
protected void initColors() {
BACKGROUND_COLOR = getResources().getColor(R.color.background_material_light);
DESCRIPTION_COLOR = getResources().getColor(R.color.primarytext);
CHART_TEXT_COLOR = getResources().getColor(R.color.secondarytext);
LEGEND_TEXT_COLOR = getResources().getColor(R.color.primarytext);
AK_ACTIVITY_COLOR = getResources().getColor(R.color.chart_activity_light);
AK_DEEP_SLEEP_COLOR = getResources().getColor(R.color.chart_light_sleep_light);
AK_LIGHT_SLEEP_COLOR = getResources().getColor(R.color.chart_deep_sleep_light);
AK_NOT_WORN_COLOR = getResources().getColor(R.color.chart_not_worn_light);
HEARTRATE_LABEL = getContext().getString(R.string.charts_legend_heartrate);
akActivity = new ActivityConfig(ActivityKind.TYPE_ACTIVITY, getString(R.string.abstract_chart_fragment_kind_activity), AK_ACTIVITY_COLOR);
akLightSleep = new ActivityConfig(ActivityKind.TYPE_LIGHT_SLEEP, getString(R.string.abstract_chart_fragment_kind_light_sleep), AK_LIGHT_SLEEP_COLOR);
akDeepSleep = new ActivityConfig(ActivityKind.TYPE_DEEP_SLEEP, getString(R.string.abstract_chart_fragment_kind_deep_sleep), AK_DEEP_SLEEP_COLOR);
akNotWorn = new ActivityConfig(ActivityKind.TYPE_NOT_WORN, getString(R.string.abstract_chart_fragment_kind_not_worn), AK_NOT_WORN_COLOR);
akActivity.color = AK_ACTIVITY_COLOR;
akLightSleep.color = AK_LIGHT_SLEEP_COLOR;
akDeepSleep.color = AK_DEEP_SLEEP_COLOR;
akNotWorn.color = AK_NOT_WORN_COLOR;
}
private void setStartDate(Date date) {
getChartsHost().setStartDate(date);
}
@Nullable
protected ChartsHost getChartsHost() {
return (ChartsHost) getActivity();
((ChartsHost) getHost()).setStartDate(date);
}
private void setEndDate(Date date) {
getChartsHost().setEndDate(date);
((ChartsHost) getHost()).setEndDate(date);
}
public Date getStartDate() {
return getChartsHost().getStartDate();
return ((ChartsHost) getHost()).getStartDate();
}
public Date getEndDate() {
return getChartsHost().getEndDate();
return ((ChartsHost) getHost()).getEndDate();
}
/**
@ -227,16 +169,11 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
showDateBar(true);
if (isChartDirty()) {
refresh();
}
}
protected void showDateBar(boolean show) {
getChartsHost().getDateBar().setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}
@Override
public void onDestroy() {
super.onDestroy();
@ -321,9 +258,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
return akActivity.color;
}
protected SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
protected SampleProvider getProvider(GBDevice device) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.getSampleProvider(device, db.getDaoSession());
return coordinator.getSampleProvider();
}
/**
@ -334,48 +271,51 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* @param tsFrom
* @param tsTo
*/
protected List<? extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
return provider.getAllActivitySamples(tsFrom, tsTo);
protected List<ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider provider = getProvider(device);
return db.getAllActivitySamples(tsFrom, tsTo, provider);
}
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device);
return provider.getActivitySamples(tsFrom, tsTo);
private int getTSLast24Hours(int tsTo) {
return (tsTo) - (24 * 60 * 60); // -24 hours
}
protected List<ActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider provider = getProvider(device);
return db.getActivitySamples(tsFrom, tsTo, provider);
}
protected List<? extends ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
return provider.getSleepSamples(tsFrom, tsTo);
protected List<ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider provider = getProvider(device);
return db.getSleepSamples(tsFrom, tsTo, provider);
}
protected List<ActivitySample> getTestSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2015, Calendar.JUNE, 10, 6, 40);
// ignore provided date ranges
tsTo = (int) ((cal.getTimeInMillis() / 1000));
tsFrom = tsTo - (24 * 60 * 60);
SampleProvider provider = getProvider(device);
return db.getAllActivitySamples(tsFrom, tsTo, provider);
}
protected void configureChartDefaults(Chart<?> chart) {
chart.getXAxis().setValueFormatter(new TimestampValueFormatter());
chart.getDescription().setText("");
// if enabled, the chart will always start at zero on the y-axis
chart.setNoDataText(getString(R.string.chart_no_data_synchronize));
// disable value highlighting
chart.setHighlightPerTapEnabled(false);
chart.setHighlightEnabled(false);
// enable touch gestures
chart.setTouchEnabled(true);
// commented out: this has weird bugs/sideeffects at least on WeekStepsCharts
// where only the first Day-label is drawn, because AxisRenderer.computeAxisValues(float,float)
// appears to have an overflow when calculating 'n' (number of entries)
// chart.getXAxis().setGranularity(60*5);
setupLegend(chart);
}
protected void configureBarLineChartDefaults(BarLineChartBase<?> chart) {
configureChartDefaults(chart);
if (chart instanceof BarChart) {
((BarChart) chart).setFitBars(true);
}
// enable scaling and dragging
chart.setDragEnabled(true);
@ -393,25 +333,19 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* #renderCharts
*/
protected void refresh() {
ChartsHost chartsHost = getChartsHost();
if (chartsHost != null) {
if (chartsHost.getDevice() != null) {
mChartDirty = false;
updateDateInfo(getStartDate(), getEndDate());
if (refreshTask != null && refreshTask.getStatus() != AsyncTask.Status.FINISHED) {
refreshTask.cancel(true);
}
refreshTask = createRefreshTask("Visualizing data", getActivity()).execute();
}
if (((ChartsHost) getHost()).getDevice() != null) {
mChartDirty = false;
updateDateInfo(getStartDate(), getEndDate());
createRefreshTask("Visualizing data", getActivity()).execute();
}
}
/**
* This method reads the data from the database, analyzes and prepares it for
* the charts. This will be called from a background task, so there must not be
* any UI access. #updateChartsInUIThread and #renderCharts will be automatically called after this method.
* any UI access. #renderCharts will be automatically called after this method.
*/
protected abstract ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device);
protected abstract void refreshInBackground(DBHandler db, GBDevice device);
/**
* Triggers the actual (re-) rendering of the chart.
@ -419,47 +353,44 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
*/
protected abstract void renderCharts();
protected DefaultChartsData<CombinedData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples) {
// Calendar cal = GregorianCalendar.getInstance();
// cal.clear();
TimestampTranslation tsTranslation = new TimestampTranslation();
// Date date;
// String dateStringFrom = "";
// String dateStringTo = "";
// ArrayList<String> xLabels = null;
protected void refresh(GBDevice gbDevice, BarLineChartBase chart, List<ActivitySample> samples) {
Calendar cal = GregorianCalendar.getInstance();
cal.clear();
Date date;
String dateStringFrom = "";
String dateStringTo = "";
LOG.info("" + getTitle() + ": number of samples:" + samples.size());
CombinedData combinedData;
if (samples.size() > 1) {
float movement_divisor;
boolean annotate = true;
boolean use_steps_as_movement;
SampleProvider provider = getProvider(gbDevice);
int last_type = ActivityKind.TYPE_UNKNOWN;
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
int numEntries = samples.size();
List<String> xLabels = new ArrayList<>(numEntries);
List<BarEntry> activityEntries = new ArrayList<>(numEntries);
boolean hr = supportsHeartrate(gbDevice);
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
List<Integer> colors = new ArrayList<>(numEntries); // this is kinda inefficient...
int lastHrSampleIndex = -1;
for (int i = 0; i < numEntries; i++) {
ActivitySample sample = samples.get(i);
int type = sample.getKind();
int ts = tsTranslation.shorten(sample.getTimestamp());
// System.out.println(ts);
// ts = i;
// determine start and end dates
// if (i == 0) {
// cal.setTimeInMillis(ts * 1000L); // make sure it's converted to long
// date = cal.getTime();
// dateStringFrom = dateFormat.format(date);
// } else if (i == samples.size() - 1) {
// cal.setTimeInMillis(ts * 1000L); // same here
// date = cal.getTime();
// dateStringTo = dateFormat.format(date);
// }
if (i == 0) {
cal.setTimeInMillis(sample.getTimestamp() * 1000L); // make sure it's converted to long
date = cal.getTime();
dateStringFrom = dateFormat.format(date);
} else if (i == samples.size() - 1) {
cal.setTimeInMillis(sample.getTimestamp() * 1000L); // same here
date = cal.getTime();
dateStringTo = dateFormat.format(date);
}
float movement = sample.getIntensity();
@ -485,23 +416,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// value = ((float) movement) / movement_divisor;
colors.add(akActivity.color);
}
activityEntries.add(createBarEntry(value, ts));
if (hr && isValidHeartRateValue(sample.getHeartRate())) {
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
heartrateEntries.add(createLineEntry(0, ts - 1));
}
heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
lastHrSampleIndex = ts;
}
activityEntries.add(createBarEntry(value, i));
String xLabel = "";
if (annotate) {
// cal.setTimeInMillis((ts + tsOffset) * 1000L);
// date = cal.getTime();
// String dateString = annotationDateFormat.format(date);
// xLabel = dateString;
cal.setTimeInMillis(sample.getTimestamp() * 1000L);
date = cal.getTime();
String dateString = annotationDateFormat.format(date);
xLabel = dateString;
// if (last_type != type) {
// if (isSleep(last_type) && !isSleep(type)) {
// // woken up
@ -519,39 +441,29 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// chart.getXAxis().addLimitLine(line);
// }
// }
// last_type = type;
last_type = type;
}
xLabels.add(xLabel);
}
chart.getXAxis().setValues(xLabels);
BarDataSet activitySet = createActivitySet(activityEntries, colors, "Activity");
ArrayList<BarDataSet> dataSets = new ArrayList<>();
dataSets.add(activitySet);
// create a data object with the datasets
// combinedData = new CombinedData(xLabels);
combinedData = new CombinedData();
List<IBarDataSet> list = new ArrayList<>();
list.add(activitySet);
BarData barData = new BarData(list);
barData.setBarWidth(200f);
// barData.setGroupSpace(0);
combinedData.setData(barData);
BarData data = new BarData(xLabels, dataSets);
data.setGroupSpace(0);
if (hr && heartrateEntries.size() > 0) {
LineDataSet heartrateSet = createHeartrateSet(heartrateEntries, "Heart Rate");
LineData lineData = new LineData(heartrateSet);
combinedData.setData(lineData);
}
// chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo));
chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo));
// chart.setDescriptionPosition(?, ?);
} else {
combinedData = new CombinedData();
setupLegend(chart);
chart.setData(data);
}
IAxisValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
return new DefaultChartsData(combinedData, xValueFormatter);
}
protected boolean isValidHeartRateValue(int value) {
return value > HeartRateUtils.MIN_HEART_RATE_VALUE && value < HeartRateUtils.MAX_HEART_RATE_VALUE;
}
/**
@ -563,16 +475,12 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
* @param tsTo
* @return
*/
protected abstract List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
protected abstract List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
protected abstract void setupLegend(Chart chart);
protected BarEntry createBarEntry(float value, int xValue) {
return new BarEntry(xValue, value);
}
protected Entry createLineEntry(float value, int xValue) {
return new Entry(xValue, value);
protected BarEntry createBarEntry(float value, int index) {
return new BarEntry(value, index);
}
protected BarDataSet createActivitySet(List<BarEntry> values, List<Integer> colors, String label) {
@ -589,28 +497,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
// set1.setHighLightColor(Color.rgb(128, 0, 255));
// set1.setColor(Color.rgb(89, 178, 44));
set1.setValueTextColor(CHART_TEXT_COLOR);
set1.setAxisDependency(YAxis.AxisDependency.LEFT);
return set1;
}
protected LineDataSet createHeartrateSet(List<Entry> values, String label) {
LineDataSet set1 = new LineDataSet(values, label);
set1.setLineWidth(2.2f);
set1.setColor(HEARTRATE_COLOR);
// set1.setDrawCubic(true);
set1.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
set1.setCubicIntensity(0.1f);
set1.setDrawCircles(false);
// set1.setCircleRadius(2f);
// set1.setDrawFilled(true);
// set1.setColor(getResources().getColor(android.R.color.background_light));
// set1.setCircleColor(HEARTRATE_COLOR);
// set1.setFillColor(ColorTemplate.getHoloBlue());
// set1.setHighLightColor(Color.rgb(128, 0, 255));
// set1.setColor(Color.rgb(89, 178, 44));
set1.setDrawValues(true);
set1.setValueTextColor(CHART_TEXT_COLOR);
set1.setAxisDependency(YAxis.AxisDependency.RIGHT);
return set1;
}
@ -653,28 +539,20 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
public class RefreshTask extends DBAccess {
private ChartsData chartsData;
public RefreshTask(String task, Context context) {
super(task, context);
}
@Override
protected void doInBackground(DBHandler db) {
ChartsHost chartsHost = getChartsHost();
if (chartsHost != null) {
chartsData = refreshInBackground(chartsHost, db, chartsHost.getDevice());
} else {
cancel(true);
}
refreshInBackground(db, ((ChartsHost) getHost()).getDevice());
}
@Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
FragmentActivity activity = getActivity();
FragmentActivity activity = (FragmentActivity) getActivity();
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
updateChartsnUIThread(chartsData);
renderCharts();
} else {
LOG.info("Not rendering charts because activity is not available anymore");
@ -682,8 +560,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
protected abstract void updateChartsnUIThread(ChartsData chartsData);
/**
* Returns true if the date was successfully shifted, and false if the shift
* was ignored, e.g. when the to-value is in the future.
@ -706,52 +582,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
protected void updateDateInfo(Date from, Date to) {
if (from.equals(to)) {
getChartsHost().setDateInfo(DateTimeUtils.formatDate(from));
((ChartsHost) getHost()).setDateInfo(DateTimeUtils.formatDate(from));
} else {
getChartsHost().setDateInfo(DateTimeUtils.formatDateRange(from, to));
((ChartsHost) getHost()).setDateInfo(DateTimeUtils.formatDateRange(from, to));
}
}
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
int tsStart = getTSStart();
int tsEnd = getTSEnd();
List<ActivitySample> samples = (List<ActivitySample>) getSamples(db, device, tsStart, tsEnd);
ensureStartAndEndSamples(samples, tsStart, tsEnd);
// List<ActivitySample> samples2 = new ArrayList<>();
// int min = Math.min(samples.size(), 10);
// int min = Math.min(samples.size(), 10);
// for (int i = 0; i < min; i++) {
// samples2.add(samples.get(i));
// }
// return samples2;
return samples;
}
protected void ensureStartAndEndSamples(List<ActivitySample> samples, int tsStart, int tsEnd) {
if (samples == null || samples.isEmpty()) {
return;
}
ActivitySample lastSample = samples.get(samples.size() - 1);
if (lastSample.getTimestamp() < tsEnd) {
samples.add(createTrailingActivitySample(lastSample, tsEnd));
}
ActivitySample firstSample = samples.get(0);
if (firstSample.getTimestamp() > tsStart) {
samples.add(createTrailingActivitySample(firstSample, tsStart));
}
}
private ActivitySample createTrailingActivitySample(ActivitySample referenceSample, int timestamp) {
TrailingActivitySample sample = new TrailingActivitySample();
if (referenceSample instanceof AbstractActivitySample) {
AbstractActivitySample reference = (AbstractActivitySample) referenceSample;
sample.setUserId(reference.getUserId());
sample.setDeviceId(reference.getDeviceId());
sample.setProvider(reference.getProvider());
}
sample.setTimestamp(timestamp);
return sample;
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device) {
return getSamples(db, device, getTSStart(), getTSEnd());
}
private int getTSEnd() {
@ -765,88 +603,4 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
private int toTimestamp(Date date) {
return (int) ((date.getTime() / 1000));
}
public static class DefaultChartsData<T extends ChartData<?>> extends ChartsData {
private final T data;
private IAxisValueFormatter xValueFormatter;
public DefaultChartsData(T data, IAxisValueFormatter xValueFormatter) {
this.xValueFormatter = xValueFormatter;
this.data = data;
}
public IAxisValueFormatter getXValueFormatter() {
return xValueFormatter;
}
public T getData() {
return data;
}
}
protected static class SampleXLabelFormatter implements IAxisValueFormatter {
private final TimestampTranslation tsTranslation;
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
Calendar cal = GregorianCalendar.getInstance();
public SampleXLabelFormatter(TimestampTranslation tsTranslation) {
this.tsTranslation = tsTranslation;
}
// TODO: this does not work. Cannot use precomputed labels
@Override
public String getFormattedValue(float value, AxisBase axis) {
cal.clear();
int ts = (int) value;
cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L);
Date date = cal.getTime();
String dateString = annotationDateFormat.format(date);
return dateString;
}
}
protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
private ArrayList<String> xLabels;
public PreformattedXIndexLabelFormatter(ArrayList<String> xLabels) {
this.xLabels = xLabels;
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
int index = (int) value;
if (xLabels == null || index >= xLabels.size()) {
return String.valueOf(value);
}
return xLabels.get(index);
}
}
/**
* Awkward class that helps in translating long timestamp
* values to float (sic!) values. It basically rebases all
* timestamps to a base (the very first) timestamp value.
*
* It does this so that the large timestamp values can be used
* floating point values, where the mantissa is just 24 bits.
*/
protected static class TimestampTranslation {
private int tsOffset = -1;
public int shorten(int timestamp) {
if (tsOffset == -1) {
tsOffset = timestamp;
return 0;
}
return timestamp - tsOffset;
}
public int toOriginalValue(int timestamp) {
if (tsOffset == -1) {
return timestamp;
}
return timestamp + tsOffset;
}
}
}

View File

@ -1,327 +0,0 @@
/* Copyright (C) 2015-2017 0nse, Alberto, Andreas Shimokawa, 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.activities.charts;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
private Locale mLocale;
private int mTargetValue = 0;
private PieChart mTodayPieChart;
private BarChart mWeekChart;
private int mOffsetHours = getOffsetHours();
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
day.setTime(chartsHost.getEndDate());
//NB: we could have omitted the day, but this way we can move things to the past easily
DayData dayData = refreshDayPie(db, day, device);
DefaultChartsData weekBeforeData = refreshWeekBeforeData(db, mWeekChart, day, device);
return new MyChartsData(dayData, weekBeforeData);
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
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.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter());
}
@Override
protected void renderCharts() {
mWeekChart.invalidate();
mTodayPieChart.invalidate();
}
private DefaultChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -7);
List<BarEntry> entries = new ArrayList<>();
ArrayList<String> labels = new ArrayList<String>();
for (int counter = 0; counter < 7; counter++) {
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
day.add(Calendar.DATE, 1);
}
BarDataSet set = new BarDataSet(entries, "");
set.setColors(getColors());
set.setValueFormatter(getBarValueFormatter());
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();
barChart.getAxisLeft().addLimitLine(target);
return new DefaultChartsData(barData, new PreformattedXIndexLabelFormatter(labels));
}
private DayData refreshDayPie(DBHandler db, Calendar day, GBDevice device) {
PieData data = new PieData();
List<PieEntry> entries = new ArrayList<>();
PieDataSet set = new PieDataSet(entries, "");
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
float totalValues[] = getTotalsForActivityAmounts(amounts);
String[] pieLabels = getPieLabels();
float totalValue = 0;
for (int i = 0; i < totalValues.length; i++) {
float value = totalValues[i];
totalValue += value;
entries.add(new PieEntry(value, pieLabels[i]));
}
set.setColors(getColors());
if (totalValues.length < 2) {
if (totalValue < mTargetValue) {
entries.add(new PieEntry((mTargetValue - totalValue)));
set.addColor(Color.GRAY);
}
}
data.setDataSet(set);
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));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
int goal = getGoal();
if (goal >= 0) {
mTargetValue = goal;
}
mTodayPieChart = (PieChart) rootView.findViewById(R.id.todaystepschart);
mWeekChart = (BarChart) rootView.findViewById(R.id.weekstepschart);
setupWeekChart();
setupTodayPieChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
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);
}
private void setupWeekChart() {
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mWeekChart.getDescription().setText("");
mWeekChart.setFitBars(true);
configureBarLineChartDefaults(mWeekChart);
XAxis x = mWeekChart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
x.setPosition(XAxis.XAxisPosition.BOTTOM);
YAxis y = mWeekChart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setDrawZeroLine(true);
y.setSpaceBottom(0);
y.setAxisMinimum(0);
y.setValueFormatter(getYAxisFormatter());
y.setEnabled(true);
YAxis yAxisRight = mWeekChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
day.add(Calendar.HOUR, offsetHours);
startTs = (int) (day.getTimeInMillis() / 1000);
endTs = startTs + 24 * 60 * 60 - 1;
return getSamples(db, device, startTs, endTs);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
private static class DayData {
private final PieData data;
private final CharSequence centerText;
DayData(PieData data, String centerText) {
this.data = data;
this.centerText = centerText;
}
}
private static class MyChartsData extends ChartsData {
private final DefaultChartsData<BarData> weekBeforeData;
private final DayData dayData;
MyChartsData(DayData dayData, DefaultChartsData<BarData> weekBeforeData) {
this.dayData = dayData;
this.weekBeforeData = weekBeforeData;
}
DayData getDayData() {
return dayData;
}
DefaultChartsData<BarData> getWeekBeforeData() {
return weekBeforeData;
}
}
private ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) {
LimitedQueue activityAmountCache = null;
ActivityAmounts amounts = null;
Activity activity = getActivity();
int key = (int) (day.getTimeInMillis() / 1000) + (mOffsetHours * 3600);
if (activity != null) {
activityAmountCache = ((ChartsActivity) activity).mActivityAmountCache;
amounts = (ActivityAmounts) (activityAmountCache.lookup(key));
}
if (amounts == null) {
ActivityAnalysis analysis = new ActivityAnalysis();
amounts = analysis.calculateActivityAmounts(getSamplesOfDay(db, day, mOffsetHours, device));
if (activityAmountCache != null) {
activityAmountCache.add(key, amounts);
}
}
return amounts;
}
abstract int getGoal();
abstract int getOffsetHours();
abstract float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts);
abstract String formatPieValue(int value);
abstract String[] getPieLabels();
abstract IValueFormatter getPieValueFormatter();
abstract IValueFormatter getBarValueFormatter();
abstract IAxisValueFormatter getYAxisFormatter();
abstract int[] getColors();
abstract String getPieDescription(int targetValue);
}

View File

@ -1,43 +1,14 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Vebryn
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class ActivityAnalysis {
private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class);
// store raw steps and duration
protected HashMap<Integer, Long> stats = new HashMap<Integer, Long>();
// max speed determined from samples
private int maxSpeed = 0;
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
public class ActivityAnalysis {
public ActivityAmounts calculateActivityAmounts(List<ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN);
@ -46,7 +17,7 @@ class ActivityAnalysis {
ActivityAmount previousAmount = null;
ActivitySample previousSample = null;
for (ActivitySample sample : samples) {
ActivityAmount amount;
ActivityAmount amount = null;
switch (sample.getKind()) {
case ActivityKind.TYPE_DEEP_SLEEP:
amount = deepSleep;
@ -63,11 +34,6 @@ class ActivityAnalysis {
break;
}
int steps = sample.getSteps();
if (steps > 0) {
amount.addSteps(steps);
}
if (previousSample != null) {
long timeDifference = sample.getTimestamp() - previousSample.getTimestamp();
if (previousSample.getRawKind() == sample.getRawKind()) {
@ -77,22 +43,8 @@ class ActivityAnalysis {
previousAmount.addSeconds(sharedTimeDifference);
amount.addSeconds(sharedTimeDifference);
}
// add time
if (steps > 0 && sample.getKind() == ActivityKind.TYPE_ACTIVITY) {
if (steps > maxSpeed) {
maxSpeed = steps;
}
if (!stats.containsKey(steps)) {
LOG.info("Adding: " + steps);
stats.put(steps, timeDifference);
} else {
long time = stats.get(steps);
LOG.info("Updating: " + steps + " " + timeDifference + time);
stats.put(steps, timeDifference + time);
}
}
} else {
// nothing to do, we can only calculate when we have the next sample
}
previousAmount = amount;
@ -114,13 +66,10 @@ class ActivityAnalysis {
return result;
}
int calculateTotalSteps(List<? extends ActivitySample> samples) {
public int calculateTotalSteps(List<ActivitySample> samples) {
int totalSteps = 0;
for (ActivitySample sample : samples) {
int steps = sample.getSteps();
if (steps > 0) {
totalSteps += sample.getSteps();
}
totalSteps += sample.getSteps();
}
return totalSteps;
}

View File

@ -1,20 +1,3 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, 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.activities.charts;
import android.content.Context;
@ -27,7 +10,6 @@ import android.view.ViewGroup;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
@ -38,7 +20,6 @@ import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -73,7 +54,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
private void setupChart() {
mChart.setBackgroundColor(BACKGROUND_COLOR);
mChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mChart.setDescriptionColor(DESCRIPTION_COLOR);
configureBarLineChartDefaults(mChart);
@ -88,8 +69,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
y.setDrawGridLines(false);
// y.setDrawLabels(false);
// TODO: make fixed max value optional
y.setAxisMaximum(1f);
y.setAxisMinimum(0);
y.setAxisMaxValue(1f);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
@ -98,12 +78,10 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
YAxis yAxisRight = mChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(true);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
@ -125,61 +103,34 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
}
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<? extends ActivitySample> samples = getSamples(db, device);
return refresh(device, samples);
}
protected void refreshInBackground(DBHandler db, GBDevice device) {
List<ActivitySample> samples = getSamples(db, device);
refresh(device, mChart, samples);
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
DefaultChartsData dcd = (DefaultChartsData) chartsData;
mChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
mChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mChart.getXAxis().setValueFormatter(dcd.getXValueFormatter());
mChart.setData(dcd.getData());
}
@Override
protected void renderCharts() {
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
// mChart.invalidate();
}
@Override
protected void setupLegend(Chart chart) {
List<LegendEntry> legendEntries = new ArrayList<>(5);
LegendEntry activityEntry = new LegendEntry();
activityEntry.label = akActivity.label;
activityEntry.formColor = akActivity.color;
legendEntries.add(activityEntry);
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);
LegendEntry notWornEntry = new LegendEntry();
notWornEntry.label = akNotWorn.label;
notWornEntry.formColor = akNotWorn.color;
legendEntries.add(notWornEntry);
if (supportsHeartrate(getChartsHost().getDevice())) {
LegendEntry hrEntry = new LegendEntry();
hrEntry.label = HEARTRATE_LABEL;
hrEntry.formColor = HEARTRATE_COLOR;
legendEntries.add(hrEntry);
}
chart.getLegend().setCustom(legendEntries);
List<Integer> legendColors = new ArrayList<>(4);
List<String> legendLabels = new ArrayList<>(4);
legendColors.add(akActivity.color);
legendLabels.add(akActivity.label);
legendColors.add(akLightSleep.color);
legendLabels.add(akLightSleep.label);
legendColors.add(akDeepSleep.color);
legendLabels.add(akDeepSleep.label);
legendColors.add(akNotWorn.color);
legendLabels.add(akNotWorn.label);
chart.getLegend().setColors(legendColors);
chart.getLegend().setLabels(legendLabels);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return getAllSamples(db, device, tsFrom, tsTo);
}
}

View File

@ -1,23 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Vebryn
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -28,80 +10,47 @@ import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public class ChartsActivity extends AbstractGBFragmentActivity implements ChartsHost {
private static final Logger LOG = LoggerFactory.getLogger(ChartsActivity.class);
private ProgressBar mProgressBar;
private Button mPrevButton;
private Button mNextButton;
private TextView mDateControl;
private Date mStartDate;
private Date mEndDate;
private SwipeRefreshLayout swipeLayout;
private ViewPager viewPager;
LimitedQueue mActivityAmountCache = new LimitedQueue(60);
private static class ShowDurationDialog extends Dialog {
private final String mDuration;
private TextView durationLabel;
ShowDurationDialog(String duration, Context context) {
super(context);
mDuration = duration;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_charts_durationdialog);
durationLabel = (TextView) findViewById(R.id.charts_duration_label);
setDuration(mDuration);
}
public void setDuration(String duration) {
if (mDuration != null) {
durationLabel.setText(duration);
} else {
durationLabel.setText("");
}
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case ControlCenter.ACTION_QUIT:
finish();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
refreshBusyState(dev);
@ -110,19 +59,17 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
}
};
private GBDevice mGBDevice;
private ViewGroup dateBar;
private void refreshBusyState(GBDevice dev) {
if (dev.isBusy()) {
swipeLayout.setRefreshing(true);
mProgressBar.setVisibility(View.VISIBLE);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false);
boolean wasBusy = mProgressBar.getVisibility() != View.GONE;
if (wasBusy) {
mProgressBar.setVisibility(View.GONE);
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}
}
enableSwipeRefresh(true);
}
@Override
@ -133,6 +80,7 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
initDates();
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(ControlCenter.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
@ -143,43 +91,11 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
swipeLayout = (SwipeRefreshLayout) findViewById(R.id.activity_swipe_layout);
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
fetchActivityData();
}
});
enableSwipeRefresh(true);
// Set up the ViewPager with the sections adapter.
viewPager = (ViewPager) findViewById(R.id.charts_pager);
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
viewPager.setAdapter(getPagerAdapter());
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
enableSwipeRefresh(state == ViewPager.SCROLL_STATE_IDLE);
}
});
dateBar = (ViewGroup) findViewById(R.id.charts_date_bar);
mDateControl = (TextView) findViewById(R.id.charts_text_date);
mDateControl.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String detailedDuration = formatDetailedDuration();
new ShowDurationDialog(detailedDuration, ChartsActivity.this).show();
}
});
mProgressBar = (ProgressBar) findViewById(R.id.charts_progress);
mPrevButton = (Button) findViewById(R.id.charts_previous);
mPrevButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -195,15 +111,7 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
}
});
LinearLayout mainLayout = (LinearLayout) findViewById(R.id.charts_main_layout);
}
private String formatDetailedDuration() {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
String dateStringFrom = dateFormat.format(getStartDate());
String dateStringTo = dateFormat.format(getEndDate());
return getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo);
mDateControl = (TextView) findViewById(R.id.charts_text_date);
}
protected void initDates() {
@ -254,11 +162,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_charts, menu);
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
if (!mGBDevice.isConnected() || !coordinator.supportsActivityDataFetching()) {
menu.removeItem(R.id.charts_fetch_activity_data);
}
return true;
}
@ -266,7 +169,7 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.charts_fetch_activity_data:
fetchActivityData();
GBApplication.deviceService().onFetchActivityData();
return true;
default:
break;
@ -275,20 +178,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return super.onOptionsItemSelected(item);
}
private void enableSwipeRefresh(boolean enable) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
swipeLayout.setEnabled(enable && coordinator.allowFetchActivityData(mGBDevice));
}
private void fetchActivityData() {
if (getDevice().isInitialized()) {
GBApplication.deviceService().onFetchActivityData();
} else {
swipeLayout.setRefreshing(false);
GB.toast(this, getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.ERROR);
}
}
@Override
public void setDateInfo(String dateInfo) {
mDateControl.setText(dateInfo);
@ -299,19 +188,13 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return new SectionsPagerAdapter(fragmentManager);
}
@Override
public ViewGroup getDateBar() {
return dateBar;
}
/**
* A {@link FragmentStatePagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
public static class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
SectionsPagerAdapter(FragmentManager fm) {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@ -324,44 +207,16 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
case 1:
return new SleepChartFragment();
case 2:
return new WeekSleepChartFragment();
case 3:
return new WeekStepsChartFragment();
case 4:
return new SpeedZonesFragment();
case 5:
return new LiveActivityFragment();
}
return null;
}
@Override
public int getCount() {
// Show 5 or 6 total pages.
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
if (coordinator.supportsRealtimeData()) {
return 6;
}
return 5;
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.activity_sleepchart_activity_and_sleep);
case 1:
return getString(R.string.sleepchart_your_sleep);
case 2:
return getString(R.string.weeksleepchart_sleep_a_week);
case 3:
return getString(R.string.weekstepschart_steps_a_week);
case 4:
return getString(R.string.stats_title);
case 5:
return getString(R.string.liveactivity_live_activity);
}
return super.getPageTitle(position);
// Show 3 total pages.
return 3;
}
}
}

View File

@ -1,20 +0,0 @@
/* Copyright (C) 2016-2017 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.activities.charts;
public abstract class ChartsData {
}

View File

@ -1,43 +1,20 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.view.ViewGroup;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public interface ChartsHost {
String DATE_PREV = ChartsActivity.class.getName().concat(".date_prev");
String DATE_NEXT = ChartsActivity.class.getName().concat(".date_next");
String REFRESH = ChartsActivity.class.getName().concat(".refresh");
static final String DATE_PREV = ChartsActivity.class.getName().concat(".date_prev");
static final String DATE_NEXT = ChartsActivity.class.getName().concat(".date_next");
static final String REFRESH = ChartsActivity.class.getName().concat(".refresh");
GBDevice getDevice();
void setStartDate(Date startDate);
void setEndDate(Date endDate);
Date getStartDate();
Date getEndDate();
void setDateInfo(String dateInfo);
ViewGroup getDateBar();
}

View File

@ -1,38 +1,16 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.renderer.BarChartRenderer;
/**
* A BarChart with some specific customization, like
* <li>allowing to animate a single entry's values without going over 0</li>
* <li>using a custom legend renderer that always uses fixed labels and colors</li>
*/
public class CustomBarChart extends BarChart {
private Entry entry = null;
private SingleEntryValueAnimator singleEntryAnimator;
public CustomBarChart(Context context) {
super(context);
}
@ -45,32 +23,9 @@ public class CustomBarChart extends BarChart {
super(context, attrs, defStyle);
}
public void setSinglAnimationEntry(Entry entry) {
this.entry = entry;
if (entry != null) {
// single entry animation mode
singleEntryAnimator = new SingleEntryValueAnimator(entry, new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// ViewCompat.postInvalidateOnAnimation(Chart.this);
postInvalidate();
}
});
mAnimator = singleEntryAnimator;
mRenderer = new BarChartRenderer(this, singleEntryAnimator, getViewPortHandler());
}
}
/**
* Call this to set the next value for the Entry to be animated.
* Call animateY() when ready to do that.
*
* @param nextValue
*/
public void setSingleEntryYValue(float nextValue) {
if (singleEntryAnimator != null) {
singleEntryAnimator.setEntryYValue(nextValue);
}
@Override
protected void init() {
super.init();
mLegendRenderer = new CustomLegendRenderer(getViewPortHandler(), getLegend());
}
}

View File

@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.graphics.Typeface;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.renderer.LegendRenderer;
import com.github.mikephil.charting.utils.ViewPortHandler;
/**
* A legend renderer that does *not* calculate the labels and colors automatically
* from the data sets or the data entries.
* <p/>
* Instead, they have to be provided manually, because otherwise the legend will
* be empty.
*/
public class CustomLegendRenderer extends LegendRenderer {
public CustomLegendRenderer(ViewPortHandler viewPortHandler, Legend legend) {
super(viewPortHandler, legend);
}
@Override
public void computeLegend(ChartData<?> data) {
if (!mLegend.isEnabled()) {
return;
}
// don't call super to avoid computing colors and labels
// super.computeLegend(data);
Typeface tf = mLegend.getTypeface();
if (tf != null)
mLegendLabelPaint.setTypeface(tf);
mLegendLabelPaint.setTextSize(mLegend.getTextSize());
mLegendLabelPaint.setColor(mLegend.getTextColor());
// calculate all dimensions of the mLegend
mLegend.calculateDimensions(mLegendLabelPaint);
}
}

View File

@ -1,530 +0,0 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Paint;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.LocalBroadcastManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.Measurement;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class LiveActivityFragment extends AbstractChartFragment {
private static final Logger LOG = LoggerFactory.getLogger(LiveActivityFragment.class);
private static final int MAX_STEPS_PER_MINUTE = 300;
private static final int MIN_STEPS_PER_MINUTE = 60;
private static final int RESET_COUNT = 10; // reset the max steps per minute value every 10s
private BarEntry totalStepsEntry;
private BarEntry stepsPerMinuteEntry;
private BarDataSet mStepsPerMinuteData;
private BarDataSet mTotalStepsData;
private LineDataSet mHistorySet;
private BarLineChartBase mStepsPerMinuteHistoryChart;
private CustomBarChart mStepsPerMinuteCurrentChart;
private CustomBarChart mTotalStepsChart;
private final Steps mSteps = new Steps();
private ScheduledExecutorService pulseScheduler;
private int maxStepsResetCounter;
private List<Measurement> heartRateValues;
private LineDataSet mHeartRateSet;
private int mHeartRate;
private TimestampTranslation tsTranslation;
private class Steps {
private int steps;
private int lastTimestamp;
private int currentStepsPerMinute;
private int maxStepsPerMinute;
private int lastStepsPerMinute;
public int getStepsPerMinute(boolean reset) {
lastStepsPerMinute = currentStepsPerMinute;
int result = currentStepsPerMinute;
if (reset) {
currentStepsPerMinute = 0;
}
return result;
}
public int getTotalSteps() {
return steps;
}
public int getMaxStepsPerMinute() {
return maxStepsPerMinute;
}
public void updateCurrentSteps(int stepsDelta, int timestamp) {
try {
if (steps == 0) {
steps += stepsDelta;
lastTimestamp = timestamp;
return;
}
int timeDelta = timestamp - lastTimestamp;
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
if (currentStepsPerMinute > maxStepsPerMinute) {
maxStepsPerMinute = currentStepsPerMinute;
maxStepsResetCounter = 0;
}
steps += stepsDelta;
lastTimestamp = timestamp;
} catch (Exception ex) {
GB.toast(LiveActivityFragment.this.getContext(), ex.getMessage(), Toast.LENGTH_SHORT, GB.ERROR, ex);
}
}
private int calculateStepsPerMinute(int stepsDelta, int seconds) {
if (stepsDelta == 0) {
return 0; // not walking or not enough data per mills?
}
if (seconds <= 0) {
throw new IllegalArgumentException("delta in seconds is <= 0 -- time change?");
}
int oneMinute = 60;
float factor = oneMinute / seconds;
int result = (int) (stepsDelta * factor);
if (result > MAX_STEPS_PER_MINUTE) {
// ignore, return previous value instead
result = lastStepsPerMinute;
}
return result;
}
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case DeviceService.ACTION_REALTIME_SAMPLES: {
ActivitySample sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
addSample(sample);
break;
}
}
}
};
private void addSample(ActivitySample sample) {
int heartRate = sample.getHeartRate();
int timestamp = tsTranslation.shorten(sample.getTimestamp());
if (isValidHeartRateValue(heartRate)) {
setCurrentHeartRate(heartRate, timestamp);
}
int steps = sample.getSteps();
if (steps != ActivitySample.NOT_MEASURED) {
addEntries(steps, timestamp);
}
}
private int translateTimestampFrom(Intent intent) {
return translateTimestamp(intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()));
}
private int translateTimestamp(long tsMillis) {
int timestamp = (int) (tsMillis / 1000); // translate to seconds
return tsTranslation.shorten(timestamp); // and shorten
}
private void setCurrentHeartRate(int heartRate, int timestamp) {
addHistoryDataSet(true);
mHeartRate = heartRate;
}
private int getCurrentHeartRate() {
int result = mHeartRate;
mHeartRate = -1;
return result;
}
private void addEntries(int steps, int timestamp) {
mSteps.updateCurrentSteps(steps, timestamp);
if (++maxStepsResetCounter > RESET_COUNT) {
maxStepsResetCounter = 0;
mSteps.maxStepsPerMinute = 0;
}
// Or: count down the steps until goal reached? And then flash GOAL REACHED -> Set stretch goal
LOG.info("Steps: " + steps + ", total: " + mSteps.getTotalSteps() + ", current: " + mSteps.getStepsPerMinute(false));
// addEntries();
}
private void addEntries(int timestamp) {
mTotalStepsChart.setSingleEntryYValue(mSteps.getTotalSteps());
YAxis stepsPerMinuteCurrentYAxis = mStepsPerMinuteCurrentChart.getAxisLeft();
int maxStepsPerMinute = mSteps.getMaxStepsPerMinute();
// int extraRoom = maxStepsPerMinute/5;
// buggy in MPAndroidChart? Disable.
// stepsPerMinuteCurrentYAxis.setAxisMaxValue(Math.max(MIN_STEPS_PER_MINUTE, maxStepsPerMinute + extraRoom));
LimitLine target = new LimitLine(maxStepsPerMinute);
stepsPerMinuteCurrentYAxis.removeAllLimitLines();
stepsPerMinuteCurrentYAxis.addLimitLine(target);
int stepsPerMinute = mSteps.getStepsPerMinute(true);
mStepsPerMinuteCurrentChart.setSingleEntryYValue(stepsPerMinute);
if (!addHistoryDataSet(false)) {
return;
}
ChartData data = mStepsPerMinuteHistoryChart.getData();
if (stepsPerMinute < 0) {
stepsPerMinute = 0;
}
mHistorySet.addEntry(new Entry(timestamp, stepsPerMinute));
int hr = getCurrentHeartRate();
if (hr < 0) {
hr = 0;
}
mHeartRateSet.addEntry(new Entry(timestamp, hr));
}
private boolean addHistoryDataSet(boolean force) {
if (mStepsPerMinuteHistoryChart.getData() == null) {
// ignore the first default value to keep the "no-data-description" visible
if (force || mSteps.getTotalSteps() > 0) {
LineData data = new LineData();
data.addDataSet(mHistorySet);
data.addDataSet(mHeartRateSet);
mStepsPerMinuteHistoryChart.setData(data);
return true;
}
return false;
}
return true;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
heartRateValues = new ArrayList<>();
tsTranslation = new TimestampTranslation();
View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false);
mStepsPerMinuteCurrentChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_per_minute_current);
mTotalStepsChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_total);
mStepsPerMinuteHistoryChart = (BarLineChartBase) rootView.findViewById(R.id.livechart_steps_per_minute_history);
totalStepsEntry = new BarEntry(1, 0);
stepsPerMinuteEntry = new BarEntry(1, 0);
mStepsPerMinuteData = setupCurrentChart(mStepsPerMinuteCurrentChart, stepsPerMinuteEntry, getString(R.string.live_activity_current_steps_per_minute));
mTotalStepsData = setupTotalStepsChart(mTotalStepsChart, totalStepsEntry, getString(R.string.live_activity_total_steps));
setupHistoryChart(mStepsPerMinuteHistoryChart);
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filterLocal);
return rootView;
}
@Override
public void onPause() {
enableRealtimeTracking(false);
super.onPause();
}
@Override
public void onResume() {
super.onResume();
enableRealtimeTracking(true);
}
private ScheduledExecutorService startActivityPulse() {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
FragmentActivity activity = LiveActivityFragment.this.getActivity();
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
pulse();
}
});
}
}
}, 0, getPulseIntervalMillis(), TimeUnit.MILLISECONDS);
return service;
}
private void stopActivityPulse() {
if (pulseScheduler != null) {
pulseScheduler.shutdownNow();
pulseScheduler = null;
}
}
/**
* Called in the UI thread.
*/
private void pulse() {
addEntries(translateTimestamp(System.currentTimeMillis()));
LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData();
if (historyData == null) {
return;
}
historyData.notifyDataChanged();
mTotalStepsData.notifyDataSetChanged();
mStepsPerMinuteData.notifyDataSetChanged();
mStepsPerMinuteHistoryChart.notifyDataSetChanged();
renderCharts();
// have to enable it again and again to keep it measureing
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
}
private int getPulseIntervalMillis() {
return 1000;
}
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
enableRealtimeTracking(true);
}
private void enableRealtimeTracking(boolean enable) {
if (enable && pulseScheduler != null) {
// already running
return;
}
GBApplication.deviceService().onEnableRealtimeSteps(enable);
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(enable);
if (enable) {
if (getActivity() != null) {
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
pulseScheduler = startActivityPulse();
} else {
stopActivityPulse();
if (getActivity() != null) {
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
}
@Override
protected void onMadeInvisibleInActivity() {
enableRealtimeTracking(false);
super.onMadeInvisibleInActivity();
}
@Override
public void onDestroyView() {
onMadeInvisibleInActivity();
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
super.onDestroyView();
}
private BarDataSet setupCurrentChart(CustomBarChart chart, BarEntry entry, String title) {
mStepsPerMinuteCurrentChart.getAxisLeft().setAxisMaxValue(MAX_STEPS_PER_MINUTE);
return setupCommonChart(chart, entry, title);
}
private BarDataSet setupCommonChart(CustomBarChart chart, BarEntry entry, String title) {
chart.setSinglAnimationEntry(entry);
// chart.getXAxis().setPosition(XAxis.XAxisPosition.TOP);
chart.getXAxis().setDrawLabels(false);
chart.getXAxis().setEnabled(false);
chart.getXAxis().setTextColor(CHART_TEXT_COLOR);
chart.getAxisLeft().setTextColor(CHART_TEXT_COLOR);
chart.setBackgroundColor(BACKGROUND_COLOR);
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
chart.getDescription().setText(title);
// chart.setNoDataTextDescription("");
chart.setNoDataText("");
chart.getAxisRight().setEnabled(false);
List<BarEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
entries.add(new BarEntry(0, 0));
entries.add(entry);
entries.add(new BarEntry(2, 0));
colors.add(akActivity.color);
colors.add(akActivity.color);
colors.add(akActivity.color);
// //we don't want labels
// xLabels.add("");
// xLabels.add("");
// xLabels.add("");
BarDataSet set = new BarDataSet(entries, "");
set.setDrawValues(false);
set.setColors(colors);
BarData data = new BarData(set);
// data.setGroupSpace(0);
chart.setData(data);
chart.getLegend().setEnabled(false);
return set;
}
private BarDataSet setupTotalStepsChart(CustomBarChart chart, BarEntry entry, String label) {
mTotalStepsChart.getAxisLeft().setAxisMaximum(5000); // TODO: use daily goal - already reached steps
return setupCommonChart(chart, entry, label); // at the moment, these look the same
}
private void setupHistoryChart(BarLineChartBase chart) {
configureBarLineChartDefaults(chart);
chart.setTouchEnabled(false); // no zooming or anything, because it's updated all the time
chart.setBackgroundColor(BACKGROUND_COLOR);
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
chart.getDescription().setText(getString(R.string.live_activity_steps_per_minute_history));
chart.setNoDataText(getString(R.string.live_activity_start_your_activity));
chart.getLegend().setEnabled(false);
Paint infoPaint = chart.getPaint(Chart.PAINT_INFO);
infoPaint.setTextSize(Utils.convertDpToPixel(20f));
infoPaint.setFakeBoldText(true);
chart.setPaint(infoPaint, Chart.PAINT_INFO);
XAxis x = chart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
YAxis y = chart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setEnabled(true);
y.setAxisMinimum(0);
YAxis yAxisRight = chart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(true);
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
mHistorySet = new LineDataSet(new ArrayList<Entry>(), getString(R.string.live_activity_steps_history));
mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT);
mHistorySet.setColor(akActivity.color);
mHistorySet.setDrawCircles(false);
mHistorySet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
mHistorySet.setDrawFilled(true);
mHistorySet.setDrawValues(false);
mHeartRateSet = createHeartrateSet(new ArrayList<Entry>(), getString(R.string.live_activity_heart_rate));
mHeartRateSet.setDrawValues(false);
}
@Override
public String getTitle() {
return getContext().getString(R.string.liveactivity_live_activity);
}
@Override
protected void showDateBar(boolean show) {
// never show the data bar
super.showDateBar(false);
}
@Override
protected void refresh() {
// do nothing, we don't have any db interaction
}
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
throw new UnsupportedOperationException();
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
throw new UnsupportedOperationException();
}
@Override
protected void renderCharts() {
mStepsPerMinuteCurrentChart.animateY(150);
mTotalStepsChart.animateY(150);
mStepsPerMinuteHistoryChart.invalidate();
}
@Override
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
throw new UnsupportedOperationException("no db access supported for live activity");
}
@Override
protected void setupLegend(Chart chart) {
// no legend
}
}

View File

@ -1,70 +0,0 @@
/* Copyright (C) 2015-2017 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.activities.charts;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import com.github.mikephil.charting.animation.ChartAnimator;
import com.github.mikephil.charting.data.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SingleEntryValueAnimator extends ChartAnimator {
private static final Logger LOG = LoggerFactory.getLogger(SingleEntryValueAnimator.class);
private final Entry entry;
private final ValueAnimator.AnimatorUpdateListener listener;
private float previousValue;
public SingleEntryValueAnimator(Entry singleEntry, ValueAnimator.AnimatorUpdateListener listener) {
super(listener);
this.listener = listener;
entry = singleEntry;
}
public void setEntryYValue(float value) {
this.previousValue = entry.getY();
entry.setY(value);
}
@Override
public void animateY(int durationMillis) {
// we start with the previous value and animate the change to the
// next value.
// as our animation values are not used as absolute values, but as factors,
// we have to calculate the proper factors in advance. The entry already has
// the new value, so we create a factor to calculate the old value from the
// new value.
float startAnim;
float endAnim = 1f;
if (entry.getY() == 0f) {
startAnim = 0f;
} else {
startAnim = previousValue / entry.getY();
}
// LOG.debug("anim factors: " + startAnim + ", " + endAnim);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "phaseY", startAnim, endAnim);
animatorY.setDuration(durationMillis);
animatorY.addUpdateListener(listener);
animatorY.start();
}
}

View File

@ -1,20 +1,3 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, 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.activities.charts;
import android.content.Context;
@ -25,19 +8,15 @@ import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.CombinedChart;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.CombinedData;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import com.github.mikephil.charting.utils.ValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -47,12 +26,10 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
@ -60,7 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class SleepChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class);
private CombinedChart mActivityChart;
private BarLineChartBase mActivityChart;
private PieChart mSleepAmountChart;
private int mSmartAlarmFrom = -1;
@ -69,61 +46,41 @@ public class SleepChartFragment extends AbstractChartFragment {
private int mSmartAlarmGoneOff = -1;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<? extends ActivitySample> samples = getSamples(db, device);
protected void refreshInBackground(DBHandler db, GBDevice device) {
List<ActivitySample> samples = getSamples(db, device);
MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples);
DefaultChartsData chartsData = refresh(device, samples);
return new MyChartsData(mySleepChartsData, chartsData);
refresh(device, mActivityChart, samples);
refreshSleepAmounts(device, mSleepAmountChart, samples);
}
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<? extends ActivitySample> samples) {
private void refreshSleepAmounts(GBDevice mGBDevice, PieChart pieChart, List<ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(amounts.getTotalSeconds(), TimeUnit.SECONDS);
pieChart.setCenterText(totalSleep);
PieData data = new PieData();
List<PieEntry> entries = new ArrayList<>();
List<Entry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
// int index = 0;
long totalSeconds = 0;
int index = 0;
for (ActivityAmount amount : amounts.getAmounts()) {
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
long value = amount.getTotalSeconds();
totalSeconds += value;
// entries.add(new PieEntry(value, index++));
entries.add(new PieEntry(value, amount.getName(getActivity())));
colors.add(getColorFor(amount.getActivityKind()));
// data.addXValue(amount.getName(getActivity()));
}
long value = amount.getTotalSeconds();
entries.add(new Entry(amount.getTotalSeconds(), index++));
colors.add(getColorFor(amount.getActivityKind()));
data.addXValue(amount.getName(getActivity()));
}
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS);
PieDataSet set = new PieDataSet(entries, "");
set.setValueFormatter(new IValueFormatter() {
set.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
public String getFormattedValue(float value) {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
}
});
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);
pieChart.setData(data);
pieChart.getLegend().setEnabled(false);
//setupLegend(pieChart);
return new MySleepChartsData(totalSleep, data);
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep());
mSleepAmountChart.setData(mcd.getPieData().getPieData());
mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter());
mActivityChart.setData(mcd.getChartsData().getData());
}
@Override
@ -136,7 +93,7 @@ public class SleepChartFragment extends AbstractChartFragment {
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
mActivityChart = (CombinedChart) rootView.findViewById(R.id.sleepchart);
mActivityChart = (BarLineChartBase) rootView.findViewById(R.id.sleepchart);
mSleepAmountChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
setupActivityChart();
@ -165,17 +122,15 @@ 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.setDescriptionColor(DESCRIPTION_COLOR);
mSleepAmountChart.setDescription("");
mSleepAmountChart.setNoDataTextDescription("");
mSleepAmountChart.setNoDataText("");
mSleepAmountChart.getLegend().setEnabled(false);
}
private void setupActivityChart() {
mActivityChart.setBackgroundColor(BACKGROUND_COLOR);
mActivityChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mActivityChart.setDescriptionColor(DESCRIPTION_COLOR);
configureBarLineChartDefaults(mActivityChart);
XAxis x = mActivityChart.getXAxis();
@ -189,8 +144,7 @@ public class SleepChartFragment extends AbstractChartFragment {
y.setDrawGridLines(false);
// y.setDrawLabels(false);
// TODO: make fixed max value optional
y.setAxisMaximum(1f);
y.setAxisMinimum(0);
y.setAxisMaxValue(1f);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
@ -199,83 +153,31 @@ public class SleepChartFragment extends AbstractChartFragment {
YAxis yAxisRight = mActivityChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
yAxisRight.setDrawLabels(true);
yAxisRight.setDrawTopYLabelEntry(true);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE);
yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE);
}
@Override
protected void setupLegend(Chart chart) {
List<LegendEntry> legendEntries = new ArrayList<>(3);
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);
if (supportsHeartrate(getChartsHost().getDevice())) {
LegendEntry hrEntry = new LegendEntry();
hrEntry.label = HEARTRATE_LABEL;
hrEntry.formColor = HEARTRATE_COLOR;
legendEntries.add(hrEntry);
}
chart.getLegend().setCustom(legendEntries);
List<Integer> legendColors = new ArrayList<>(2);
List<String> legendLabels = new ArrayList<>(2);
legendColors.add(akLightSleep.color);
legendLabels.add(akLightSleep.label);
legendColors.add(akDeepSleep.color);
legendLabels.add(akDeepSleep.label);
chart.getLegend().setColors(legendColors);
chart.getLegend().setLabels(legendLabels);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
// temporary fix for totally wrong sleep amounts
// return super.getSleepSamples(db, device, tsFrom, tsTo);
return super.getAllSamples(db, device, tsFrom, tsTo);
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getSleepSamples(db, device, tsFrom, tsTo);
}
@Override
protected void renderCharts() {
mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mSleepAmountChart.invalidate();
}
private static class MySleepChartsData extends ChartsData {
private String totalSleep;
private final PieData pieData;
public MySleepChartsData(String totalSleep, PieData pieData) {
this.totalSleep = totalSleep;
this.pieData = pieData;
}
public PieData getPieData() {
return pieData;
}
public CharSequence getTotalSleep() {
return totalSleep;
}
}
private static class MyChartsData extends ChartsData {
private final DefaultChartsData<CombinedData> chartsData;
private final MySleepChartsData pieData;
public MyChartsData(MySleepChartsData pieData, DefaultChartsData<CombinedData> chartsData) {
this.pieData = pieData;
this.chartsData = chartsData;
}
public MySleepChartsData getPieData() {
return pieData;
}
public DefaultChartsData<CombinedData> getChartsData() {
return chartsData;
}
}
}

View File

@ -1,19 +1,3 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -22,7 +6,7 @@ public class SleepUtils {
public static final float Y_VALUE_DEEP_SLEEP = 0.01f;
public static final float Y_VALUE_LIGHT_SLEEP = 0.016f;
public static boolean isSleep(byte type) {
public static final boolean isSleep(byte type) {
return type == ActivityKind.TYPE_DEEP_SLEEP || type == ActivityKind.TYPE_LIGHT_SLEEP;
}
}

View File

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

View File

@ -1,51 +0,0 @@
/* Copyright (C) 2016-2017 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.activities.charts;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class TimestampValueFormatter implements IAxisValueFormatter {
private final Calendar cal;
// private DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
private DateFormat dateFormat;
public TimestampValueFormatter() {
this(new SimpleDateFormat("HH:mm"));
}
public TimestampValueFormatter(DateFormat dateFormat) {
this.dateFormat = dateFormat;
cal = GregorianCalendar.getInstance();
cal.clear();
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
cal.setTimeInMillis((int) value * 1000L);
Date date = cal.getTime();
String dateString = dateFormat.format(date);
return dateString;
}
}

View File

@ -1,55 +0,0 @@
/* Copyright (C) 2016-2017 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.activities.charts;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
public class TrailingActivitySample extends AbstractActivitySample {
private int timestamp;
private long userId;
private long deviceId;
@Override
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
@Override
public void setUserId(long userId) {
this.userId = userId;
}
@Override
public void setDeviceId(long deviceId) {
this.deviceId = deviceId;
}
@Override
public long getDeviceId() {
return deviceId;
}
@Override
public long getUserId() {
return userId;
}
@Override
public int getTimestamp() {
return timestamp;
}
}

View File

@ -1,136 +0,0 @@
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.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;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weeksleepchart_sleep_a_week);
}
@Override
String getPieDescription(int targetValue) {
return getString(R.string.weeksleepchart_today_sleep_description, DateTimeUtils.formatDurationHoursMinutes(targetValue, TimeUnit.MINUTES));
}
@Override
int getGoal() {
return GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_SLEEP_DURATION, 8) * 60;
}
@Override
int getOffsetHours() {
return -12;
}
@Override
float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts) {
long totalSecondsDeepSleep = 0;
long totalSecondsLightSleep = 0;
for (ActivityAmount amount : activityAmounts.getAmounts()) {
if (amount.getActivityKind() == ActivityKind.TYPE_DEEP_SLEEP) {
totalSecondsDeepSleep += amount.getTotalSeconds();
} else if (amount.getActivityKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
totalSecondsLightSleep += amount.getTotalSeconds();
}
}
return new float[]{(int) (totalSecondsDeepSleep / 60), (int) (totalSecondsLightSleep / 60)};
}
@Override
protected String formatPieValue(int value) {
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() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
return formatPieValue((int) value);
}
};
}
@Override
IValueFormatter getBarValueFormatter() {
return new IValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
}
@Override
IAxisValueFormatter getYAxisFormatter() {
return new IAxisValueFormatter() {
@Override
public String getFormattedValue(float value, AxisBase axis) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
}
@Override
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

@ -1,96 +1,224 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, 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.activities.charts;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class WeekStepsChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(WeekStepsChartFragment.class);
private Locale mLocale;
private int mTargetSteps = 10000;
private BarLineChartBase mWeekStepsChart;
private PieChart mTodayStepsChart;
@Override
protected void refreshInBackground(DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
day.setTime(((ChartsHost) getHost()).getEndDate());
//NB: we could have omitted the day, but this way we can move things to the past easily
refreshDaySteps(db, mTodayStepsChart, day, device);
refreshWeekBeforeSteps(db, mWeekStepsChart, day, device);
}
@Override
protected void renderCharts() {
mWeekStepsChart.invalidate();
mTodayStepsChart.invalidate();
}
private void refreshWeekBeforeSteps(DBHandler db, BarLineChartBase barChart, Calendar day, GBDevice device) {
ActivityAnalysis analysis = new ActivityAnalysis();
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -7);
List<BarEntry> entries = new ArrayList<>();
List<String> labels = new ArrayList<>();
for (int counter = 0; counter < 7; counter++) {
entries.add(new BarEntry(analysis.calculateTotalSteps(getSamplesOfDay(db, day, device)), counter));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
day.add(Calendar.DATE, 1);
}
BarDataSet set = new BarDataSet(entries, "");
set.setColor(akActivity.color);
BarData data = new BarData(labels, set);
data.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);
LimitLine target = new LimitLine(mTargetSteps);
barChart.getAxisLeft().getLimitLines().clear();
barChart.getAxisLeft().addLimitLine(target);
setupLegend(barChart);
barChart.setData(data);
barChart.getLegend().setEnabled(false);
}
private void refreshDaySteps(DBHandler db, PieChart pieChart, Calendar day, GBDevice device) {
ActivityAnalysis analysis = new ActivityAnalysis();
int totalSteps = analysis.calculateTotalSteps(getSamplesOfDay(db, day, device));
pieChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(totalSteps));
PieData data = new PieData();
List<Entry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
entries.add(new Entry(totalSteps, 0));
colors.add(akActivity.color);
//we don't want labels on the pie chart
data.addXValue("");
if (totalSteps < mTargetSteps) {
entries.add(new Entry((mTargetSteps - totalSteps), 1));
colors.add(Color.GRAY);
//we don't want labels on the pie chart
data.addXValue("");
}
PieDataSet set = new PieDataSet(entries, "");
set.setColors(colors);
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);
pieChart.setData(data);
pieChart.getLegend().setEnabled(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
GBDevice device = ((ChartsHost) getHost()).getDevice();
if (device != null) {
// TODO: eek, this is device specific!
mTargetSteps = MiBandCoordinator.getFitnessGoal(device.getAddress());
}
mWeekStepsChart = (BarLineChartBase) rootView.findViewById(R.id.sleepchart);
mTodayStepsChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
setupWeekStepsChart();
setupTodayStepsChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
public class WeekStepsChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weekstepschart_steps_a_week);
}
@Override
String getPieDescription(int targetValue) {
return getString(R.string.weeksteps_today_steps_description, String.valueOf(targetValue));
private void setupTodayStepsChart() {
mTodayStepsChart.setBackgroundColor(BACKGROUND_COLOR);
mTodayStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
mTodayStepsChart.setDescription(getContext().getString(R.string.weeksteps_today_steps_description, mTargetSteps));
mTodayStepsChart.setNoDataTextDescription("");
mTodayStepsChart.setNoDataText("");
}
@Override
int getGoal() {
return GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, 10000);
private void setupWeekStepsChart() {
mWeekStepsChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
mWeekStepsChart.setDescription("");
configureBarLineChartDefaults(mWeekStepsChart);
XAxis x = mWeekStepsChart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
YAxis y = mWeekStepsChart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setEnabled(true);
YAxis yAxisRight = mWeekStepsChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
@Override
int getOffsetHours() {
return 0;
}
@Override
float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts) {
int totalSteps = 0;
for (ActivityAmount amount : activityAmounts.getAmounts()) {
totalSteps += amount.getTotalSteps();
amount.getTotalSteps();
}
return new float[]{totalSteps};
}
@Override
protected String formatPieValue(int value) {
return String.valueOf(value);
}
@Override
String[] getPieLabels() {
return new String[]{""};
}
@Override
IValueFormatter getPieValueFormatter() {
return null;
}
@Override
IValueFormatter getBarValueFormatter() {
return null;
}
@Override
IAxisValueFormatter getYAxisFormatter() {
return null;
}
@Override
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);
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().setColors(legendColors);
chart.getLegend().setLabels(legendLabels);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
}
private List<ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, GBDevice device) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
startTs = (int) (day.getTimeInMillis() / 1000);
day.set(Calendar.HOUR_OF_DAY, 23);
day.set(Calendar.MINUTE, 59);
day.set(Calendar.SECOND, 59);
endTs = (int) (day.getTimeInMillis() / 1000);
return getSamples(db, device, startTs, endTs);
}
@Override
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
}

View File

@ -1,189 +0,0 @@
/* 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.appIsBlacklisted(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.appIsBlacklisted(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.addAppToBlacklist(appInfo.packageName);
} else {
GBApplication.removeFromAppsBlacklist(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

@ -1,19 +1,3 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
@ -26,14 +10,11 @@ import android.widget.TextView;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
/**
* Adapter for displaying GBDeviceCandate instances.
*/
public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
private final Context context;
@ -61,7 +42,17 @@ public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
String name = formatDeviceCandidate(device);
deviceNameLabel.setText(name);
deviceAddressLabel.setText(device.getMacAddress());
deviceImageView.setImageResource(device.getDeviceType().getIcon());
switch (device.getDeviceType()) {
case PEBBLE:
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
break;
case MIBAND:
deviceImageView.setImageResource(R.drawable.ic_device_miband);
break;
default:
deviceImageView.setImageResource(R.drawable.ic_launcher);
}
return view;
}

View File

@ -1,61 +1,44 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, 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.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import android.widget.ArrayAdapter;
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.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
/**
* Adapter for displaying GBAlarm instances.
*/
public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.ViewHolder> {
public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
private final Context mContext;
private List<GBAlarm> alarmList;
private ArrayList<GBAlarm> alarmList;
public GBAlarmListAdapter(Context context, ArrayList<GBAlarm> alarmList) {
super(context, 0, 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<>();
alarmList = new ArrayList<GBAlarm>();
for (String alarmString : preferencesAlarmListSet) {
alarmList.add(new GBAlarm(alarmString));
@ -64,21 +47,18 @@ public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.
Collections.sort(alarmList);
}
public void setAlarmList(Set<String> preferencesAlarmListSet, int reservedSlots) {
alarmList = new ArrayList<>();
public void setAlarmList(Set<String> preferencesAlarmListSet) {
alarmList = new ArrayList<GBAlarm>();
for (String alarmString : preferencesAlarmListSet) {
alarmList.add(new GBAlarm(alarmString));
}
Collections.sort(alarmList);
//cannot do this earlier because the Set is not guaranteed to be in order by ID
alarmList.subList(alarmList.size() - reservedSlots, alarmList.size()).clear();
}
public ArrayList<? extends Alarm> getAlarmList() {
return (ArrayList) alarmList;
return alarmList;
}
@ -92,26 +72,53 @@ public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.
}
@Override
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;
public int getCount() {
if (alarmList != null) {
return alarmList.size();
}
return 0;
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
public GBAlarm getItem(int position) {
if (alarmList != null) {
return alarmList.get(position);
}
return null;
}
final GBAlarm alarm = alarmList.get(position);
@Override
public long getItemId(int position) {
if (alarmList != null) {
return alarmList.get(position).getIndex();
}
return 0;
}
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));
@Override
public View getView(int position, View view, ViewGroup parent) {
holder.isEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
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() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
alarm.setEnabled(isChecked);
@ -119,62 +126,28 @@ public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.
}
});
holder.container.setOnClickListener(new View.OnClickListener() {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((ConfigureAlarms) mContext).configureAlarm(alarm);
}
});
holder.alarmTime.setText(alarm.getTime());
holder.isEnabled.setChecked(alarm.isEnabled());
alarmTime.setText(alarm.getTime());
isEnabled.setChecked(alarm.isEnabled());
if (alarm.isSmartWakeup()) {
holder.isSmartWakeup.setVisibility(TextView.VISIBLE);
isSmartWakeup.setVisibility(TextView.VISIBLE);
} else {
holder.isSmartWakeup.setVisibility(TextView.GONE);
isSmartWakeup.setVisibility(TextView.GONE);
}
return view;
}
private void highlightDay(TextView view, boolean isOn) {
if (isOn) {
view.setTextColor(Color.BLUE);
} else {
view.setTextColor(Color.BLACK);
}
}
@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

@ -0,0 +1,93 @@
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
import android.graphics.Color;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
private final Context context;
public GBDeviceAdapter(Context context, List<GBDevice> deviceList) {
super(context, 0, deviceList);
this.context = context;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
GBDevice device = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.device_item, parent, false);
}
TextView deviceStatusLabel = (TextView) view.findViewById(R.id.device_status);
TextView deviceNameLabel = (TextView) view.findViewById(R.id.device_name);
TextView deviceInfoLabel = (TextView) view.findViewById(R.id.device_info);
TextView batteryStatusLabel = (TextView) view.findViewById(R.id.battery_status);
ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_image);
ProgressBar busyIndicator = (ProgressBar) view.findViewById(R.id.device_busy_indicator);
deviceNameLabel.setText(device.getName());
deviceInfoLabel.setText(device.getInfoString());
if (device.isBusy()) {
deviceStatusLabel.setText(device.getBusyTask());
busyIndicator.setVisibility(View.VISIBLE);
batteryStatusLabel.setVisibility(View.GONE);
deviceInfoLabel.setVisibility(View.GONE);
} else {
deviceStatusLabel.setText(device.getStateString());
busyIndicator.setVisibility(View.GONE);
batteryStatusLabel.setVisibility(View.VISIBLE);
deviceInfoLabel.setVisibility(View.VISIBLE);
}
short batteryLevel = device.getBatteryLevel();
if (batteryLevel != GBDevice.BATTERY_UNKNOWN) {
batteryStatusLabel.setText("BAT: " + device.getBatteryLevel() + "%");
BatteryState batteryState = device.getBatteryState();
if (BatteryState.BATTERY_LOW.equals(batteryState)) {
batteryStatusLabel.setTextColor(Color.RED);
} else {
batteryStatusLabel.setTextColor(ContextCompat.getColor(getContext(), R.color.secondarytext));
if (BatteryState.BATTERY_CHARGING.equals(batteryState) ||
BatteryState.BATTERY_CHARGING_FULL.equals(batteryState)) {
batteryStatusLabel.append(" CHG");
}
}
} else {
batteryStatusLabel.setText("");
}
switch (device.getType()) {
case PEBBLE:
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
break;
case MIBAND:
deviceImageView.setImageResource(R.drawable.ic_device_miband);
break;
default:
deviceImageView.setImageResource(R.drawable.ic_launcher);
}
return view;
}
}

View File

@ -1,459 +0,0 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
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.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.support.design.widget.Snackbar;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.transition.TransitionManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.AudioSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* Adapter for displaying GBDevice instances.
*/
public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.ViewHolder> {
private final Context context;
private List<GBDevice> deviceList;
private int expandedDevicePosition = RecyclerView.NO_POSITION;
private ViewGroup parent;
public GBDeviceAdapterv2(Context context, List<GBDevice> deviceList) {
this.context = context;
this.deviceList = deviceList;
}
@Override
public GBDeviceAdapterv2.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
this.parent = parent;
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.device_itemv2, parent, false);
ViewHolder vh = new ViewHolder(view);
return vh;
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
final GBDevice device = deviceList.get(position);
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
holder.container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (device.isInitialized() || device.isConnected()) {
showTransientSnackbar(R.string.controlcenter_snackbar_need_longpress);
} else {
showTransientSnackbar(R.string.controlcenter_snackbar_connecting);
GBApplication.deviceService().connect(device);
}
}
});
holder.container.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (device.getState() != GBDevice.State.NOT_CONNECTED) {
showTransientSnackbar(R.string.controlcenter_snackbar_disconnecting);
GBApplication.deviceService().disconnect();
}
return true;
}
});
holder.deviceImageView.setImageResource(R.drawable.level_list_device);
//level-list does not allow negative values, hence we always add 100 to the key.
holder.deviceImageView.setImageLevel(device.getType().getKey() + 100 + (device.isInitialized() ? 100 : 0));
holder.deviceNameLabel.setText(getUniqueDeviceName(device));
if (device.isBusy()) {
holder.deviceStatusLabel.setText(device.getBusyTask());
holder.busyIndicator.setVisibility(View.VISIBLE);
} else {
holder.deviceStatusLabel.setText(device.getStateString());
holder.busyIndicator.setVisibility(View.INVISIBLE);
}
//begin of action row
//battery
holder.batteryStatusBox.setVisibility(View.GONE);
short batteryLevel = device.getBatteryLevel();
if (batteryLevel != GBDevice.BATTERY_UNKNOWN) {
holder.batteryStatusBox.setVisibility(View.VISIBLE);
holder.batteryStatusLabel.setText(device.getBatteryLevel() + "%");
BatteryState batteryState = device.getBatteryState();
if (BatteryState.BATTERY_CHARGING.equals(batteryState) ||
BatteryState.BATTERY_CHARGING_FULL.equals(batteryState)) {
holder.batteryIcon.setImageLevel(device.getBatteryLevel() + 100);
} else {
holder.batteryIcon.setImageLevel(device.getBatteryLevel());
}
}
//fetch activity data
holder.fetchActivityDataBox.setVisibility((device.isInitialized() && coordinator.supportsActivityDataFetching()) ? View.VISIBLE : View.GONE);
holder.fetchActivityData.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
showTransientSnackbar(R.string.busy_task_fetch_activity_data);
GBApplication.deviceService().onFetchActivityData();
}
}
);
//take screenshot
holder.takeScreenshotView.setVisibility((device.isInitialized() && coordinator.supportsScreenshots()) ? View.VISIBLE : View.GONE);
holder.takeScreenshotView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
showTransientSnackbar(R.string.controlcenter_snackbar_requested_screenshot);
GBApplication.deviceService().onScreenshotReq();
}
}
);
//manage apps
holder.manageAppsView.setVisibility((device.isInitialized() && coordinator.supportsAppsManagement()) ? View.VISIBLE : View.GONE);
holder.manageAppsView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
Class<? extends Activity> appsManagementActivity = coordinator.getAppsManagementActivity();
if (appsManagementActivity != null) {
Intent startIntent = new Intent(context, appsManagementActivity);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
}
}
);
//set alarms
holder.setAlarmsView.setVisibility(coordinator.supportsAlarmConfiguration() ? View.VISIBLE : View.GONE);
holder.setAlarmsView.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
Intent startIntent;
startIntent = new Intent(context, ConfigureAlarms.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
}
);
//show graphs
holder.showActivityGraphs.setVisibility(coordinator.supportsActivityTracking() ? View.VISIBLE : View.GONE);
holder.showActivityGraphs.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
Intent startIntent;
startIntent = new Intent(context, ChartsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
}
);
// audio settings
holder.showAudioSettings.setVisibility(device.isInitialized() && coordinator.supportsAudioSettings() ? View.VISIBLE : View.GONE);
holder.showAudioSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showTransientSnackbar(R.string.controlcenter_snackbar_requested_screenshot);
Intent startIntent;
startIntent = new Intent(context, AudioSettingsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
}
);
ItemWithDetailsAdapter infoAdapter = new ItemWithDetailsAdapter(context, device.getDeviceInfos());
infoAdapter.setHorizontalAlignment(true);
holder.deviceInfoList.setAdapter(infoAdapter);
justifyListViewHeightBasedOnChildren(holder.deviceInfoList);
holder.deviceInfoList.setFocusable(false);
final boolean detailsShown = position == expandedDevicePosition;
boolean showInfoIcon = device.hasDeviceInfos() && !device.isBusy();
holder.deviceInfoView.setVisibility(showInfoIcon ? View.VISIBLE : View.GONE);
holder.deviceInfoBox.setActivated(detailsShown);
holder.deviceInfoBox.setVisibility(detailsShown ? View.VISIBLE : View.GONE);
holder.deviceInfoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
expandedDevicePosition = detailsShown ? -1 : position;
TransitionManager.beginDelayedTransition(parent);
notifyDataSetChanged();
}
}
);
holder.findDevice.setVisibility((device.isInitialized() &&
device.getType() != DeviceType.HERE) ? View.VISIBLE : View.GONE);
holder.findDevice.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
if (device.getType() == DeviceType.VIBRATISSIMO) {
Intent startIntent;
startIntent = new Intent(context, VibrationActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
return;
}
GBApplication.deviceService().onFindDevice(true);
//TODO: extract string resource if we like this solution.
Snackbar.make(parent, R.string.control_center_find_lost_device, Snackbar.LENGTH_INDEFINITE).setAction("Found it!", new View.OnClickListener() {
@Override
public void onClick(View v) {
GBApplication.deviceService().onFindDevice(false);
}
}).setCallback(new Snackbar.Callback() {
@Override
public void onDismissed(Snackbar snackbar, int event) {
GBApplication.deviceService().onFindDevice(false);
super.onDismissed(snackbar, event);
}
}).show();
// ProgressDialog.show(
// context,
// context.getString(R.string.control_center_find_lost_device),
// context.getString(R.string.control_center_cancel_to_stop_vibration),
// true, true,
// new DialogInterface.OnCancelListener() {
// @Override
// public void onCancel(DialogInterface dialog) {
// GBApplication.deviceService().onFindDevice(false);
// }
// });
}
}
);
//remove device, hidden under details
holder.removeDevice.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
new AlertDialog.Builder(context)
.setCancelable(true)
.setTitle(context.getString(R.string.controlcenter_delete_device_name, device.getName()))
.setMessage(R.string.controlcenter_delete_device_dialogmessage)
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
if (coordinator != null) {
coordinator.deleteDevice(device);
}
DeviceHelper.getInstance().removeBond(device);
} catch (Exception ex) {
GB.toast(context, "Error deleting device: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
} finally {
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
}
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// do nothing
}
})
.show();
}
});
}
@Override
public int getItemCount() {
return deviceList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
CardView container;
ImageView deviceImageView;
TextView deviceNameLabel;
TextView deviceStatusLabel;
//actions
LinearLayout batteryStatusBox;
TextView batteryStatusLabel;
ImageView batteryIcon;
LinearLayout fetchActivityDataBox;
ImageView fetchActivityData;
ProgressBar busyIndicator;
ImageView takeScreenshotView;
ImageView manageAppsView;
ImageView setAlarmsView;
ImageView showActivityGraphs;
ImageView showAudioSettings;
ImageView deviceInfoView;
//overflow
final RelativeLayout deviceInfoBox;
ListView deviceInfoList;
ImageView findDevice;
ImageView removeDevice;
ViewHolder(View view) {
super(view);
container = (CardView) view.findViewById(R.id.card_view);
deviceImageView = (ImageView) view.findViewById(R.id.device_image);
deviceNameLabel = (TextView) view.findViewById(R.id.device_name);
deviceStatusLabel = (TextView) view.findViewById(R.id.device_status);
//actions
batteryStatusBox = (LinearLayout) view.findViewById(R.id.device_battery_status_box);
batteryStatusLabel = (TextView) view.findViewById(R.id.battery_status);
batteryIcon = (ImageView) view.findViewById(R.id.device_battery_status);
fetchActivityDataBox = (LinearLayout) view.findViewById(R.id.device_action_fetch_activity_box);
fetchActivityData = (ImageView) view.findViewById(R.id.device_action_fetch_activity);
busyIndicator = (ProgressBar) view.findViewById(R.id.device_busy_indicator);
takeScreenshotView = (ImageView) view.findViewById(R.id.device_action_take_screenshot);
manageAppsView = (ImageView) view.findViewById(R.id.device_action_manage_apps);
setAlarmsView = (ImageView) view.findViewById(R.id.device_action_set_alarms);
showActivityGraphs = (ImageView) view.findViewById(R.id.device_action_show_activity_graphs);
showAudioSettings = (ImageView) view.findViewById(R.id.device_action_show_audio_settings);
deviceInfoView = (ImageView) view.findViewById(R.id.device_info_image);
deviceInfoBox = (RelativeLayout) view.findViewById(R.id.device_item_infos_box);
//overflow
deviceInfoList = (ListView) view.findViewById(R.id.device_item_infos);
findDevice = (ImageView) view.findViewById(R.id.device_action_find);
removeDevice = (ImageView) view.findViewById(R.id.device_action_remove);
}
}
public void justifyListViewHeightBasedOnChildren(ListView listView) {
ArrayAdapter adapter = (ArrayAdapter) listView.getAdapter();
if (adapter == null) {
return;
}
ViewGroup vg = listView;
int totalHeight = 0;
for (int i = 0; i < adapter.getCount(); i++) {
View listItem = adapter.getView(i, null, vg);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams par = listView.getLayoutParams();
par.height = totalHeight + (listView.getDividerHeight() * (adapter.getCount() - 1));
listView.setLayoutParams(par);
listView.requestLayout();
}
private String getUniqueDeviceName(GBDevice device) {
String deviceName = device.getName();
if (!isUniqueDeviceName(device, deviceName)) {
if (device.getModel() != null) {
deviceName = deviceName + " " + device.getModel();
if (!isUniqueDeviceName(device, deviceName)) {
deviceName = deviceName + " " + device.getShortAddress();
}
} else {
deviceName = deviceName + " " + device.getShortAddress();
}
}
return deviceName;
}
private boolean isUniqueDeviceName(GBDevice device, String deviceName) {
for (int i = 0; i < deviceList.size(); i++) {
GBDevice item = deviceList.get(i);
if (item == device) {
continue;
}
if (deviceName.equals(item.getName())) {
return false;
}
}
return true;
}
private void showTransientSnackbar(int resource) {
Snackbar snackbar = Snackbar.make(parent, resource, Snackbar.LENGTH_SHORT);
View snackbarView = snackbar.getView();
// change snackbar text color
int snackbarTextId = android.support.design.R.id.snackbar_text;
TextView textView = (TextView) snackbarView.findViewById(snackbarTextId);
//textView.setTextColor();
//snackbarView.setBackgroundColor(Color.MAGENTA);
snackbar.show();
}
}

View File

@ -1,145 +1,55 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, 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.support.v7.widget.RecyclerView;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.R;
/**
* Adapter for displaying GBDeviceApp instances.
*/
public class GBDeviceAppAdapter extends ArrayAdapter<GBDeviceApp> {
public class GBDeviceAppAdapter extends RecyclerView.Adapter<GBDeviceAppAdapter.AppViewHolder> {
private final Context context;
private final int mLayoutId;
private final List<GBDeviceApp> appList;
private final AbstractAppManagerFragment mParentFragment;
public GBDeviceAppAdapter(Context context, List<GBDeviceApp> appList) {
super(context, 0, appList);
public List<GBDeviceApp> getAppList() {
return appList;
}
public GBDeviceAppAdapter(List<GBDeviceApp> list, int layoutId, AbstractAppManagerFragment parentFragment) {
mLayoutId = layoutId;
appList = list;
mParentFragment = parentFragment;
this.context = context;
}
@Override
public long getItemId(int position) {
return appList.get(position).getUUID().getLeastSignificantBits();
}
public View getView(int position, View view, ViewGroup parent) {
GBDeviceApp deviceApp = getItem(position);
@Override
public int getItemCount() {
return appList.size();
}
if (view == null) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@Override
public GBDeviceAppAdapter.AppViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
return new AppViewHolder(view);
}
@Override
public void onBindViewHolder(final AppViewHolder holder, int position) {
final GBDeviceApp deviceApp = appList.get(position);
holder.mDeviceAppVersionAuthorLabel.setText(GBApplication.getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
// FIXME: replace with small icons
String appNameLabelText = deviceApp.getName();
holder.mDeviceAppNameLabel.setText(appNameLabelText);
view = inflater.inflate(R.layout.item_with_details, parent, false);
}
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);
deviceAppVersionAuthorLabel.setText(getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
deviceAppNameLabel.setText(deviceApp.getName());
switch (deviceApp.getType()) {
case APP_GENERIC:
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
break;
case APP_ACTIVITYTRACKER:
holder.mDeviceImageView.setImageResource(R.drawable.ic_activitytracker);
break;
case APP_SYSTEM:
holder.mDeviceImageView.setImageResource(R.drawable.ic_systemapp);
deviceImageView.setImageResource(R.drawable.ic_activitytracker);
break;
case WATCHFACE:
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchface);
deviceImageView.setImageResource(R.drawable.ic_watchface);
break;
default:
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UUID uuid = deviceApp.getUUID();
GBApplication.deviceService().onAppStart(uuid, true);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return mParentFragment.openPopupMenu(view, deviceApp);
}
});
holder.mDragHandle.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
mParentFragment.startDragging(holder);
return true;
}
});
return view;
}
public void onItemMove(int from, int to) {
Collections.swap(appList, from, to);
notifyItemMoved(from, to);
}
public class AppViewHolder extends RecyclerView.ViewHolder {
final TextView mDeviceAppVersionAuthorLabel;
final TextView mDeviceAppNameLabel;
final ImageView mDeviceImageView;
final ImageView mDragHandle;
AppViewHolder(View itemView) {
super(itemView);
mDeviceAppVersionAuthorLabel = (TextView) itemView.findViewById(R.id.item_details);
mDeviceAppNameLabel = (TextView) itemView.findViewById(R.id.item_name);
mDeviceImageView = (ImageView) itemView.findViewById(R.id.item_image);
mDragHandle = (ImageView) itemView.findViewById(R.id.drag_handle);
}
}
}

View File

@ -1,19 +1,3 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.adapter;
import android.content.Context;
@ -29,17 +13,9 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
/**
* Adapter for displaying generic ItemWithDetails instances.
*/
public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
public static final int SIZE_SMALL = 1;
public static final int SIZE_MEDIUM = 2;
public static final int SIZE_LARGE = 3;
private final Context context;
private boolean horizontalAlignment;
private int size = SIZE_MEDIUM;
public ItemWithDetailsAdapter(Context context, List<ItemWithDetails> items) {
super(context, 0, items);
@ -47,10 +23,6 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
this.context = context;
}
public void setHorizontalAlignment(boolean horizontalAlignment) {
this.horizontalAlignment = horizontalAlignment;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
ItemWithDetails item = getItem(position);
@ -59,18 +31,7 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (horizontalAlignment) {
view = inflater.inflate(R.layout.item_with_details_horizontal, parent, false);
} else {
switch (size) {
case SIZE_SMALL:
view = inflater.inflate(R.layout.item_with_details_small, parent, false);
break;
default:
view = inflater.inflate(R.layout.item_with_details, parent, false);
break;
}
}
view = inflater.inflate(R.layout.item_with_details, parent, false);
}
ImageView iconView = (ImageView) view.findViewById(R.id.item_image);
TextView nameView = (TextView) view.findViewById(R.id.item_name);
@ -82,12 +43,4 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
return view;
}
public void setSize(int size) {
this.size = size;
}
public int getSize() {
return size;
}
}

View File

@ -1,114 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.contentprovider;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class PebbleContentProvider extends ContentProvider {
public static final int COLUMN_CONNECTED = 0;
public static final int COLUMN_APPMSG_SUPPORT = 1;
public static final int COLUMN_DATALOGGING_SUPPORT = 2;
public static final int COLUMN_VERSION_MAJOR = 3;
public static final int COLUMN_VERSION_MINOR = 4;
public static final int COLUMN_VERSION_POINT = 5;
public static final int COLUMN_VERSION_TAG = 6;
// this is only needed for the MatrixCursor constructor
public static final String[] columnNames = new String[]{"0", "1", "2", "3", "4", "5", "6"};
static final String PROVIDER_NAME = "com.getpebble.android.provider";
static final String URL = "content://" + PROVIDER_NAME + "/state";
static final Uri CONTENT_URI = Uri.parse(URL);
private GBDevice mGBDevice = null;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
}
}
};
@Override
public boolean onCreate() {
LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED));
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
if (uri.equals(CONTENT_URI)) {
MatrixCursor mc = new MatrixCursor(columnNames);
int connected = 0;
int pebbleKit = 0;
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("pebble_enable_pebblekit", false)) {
pebbleKit = 1;
}
String fwString = "unknown";
if (mGBDevice != null && mGBDevice.getType() == DeviceType.PEBBLE && mGBDevice.isInitialized()) {
connected = 1;
fwString = mGBDevice.getFirmwareVersion();
}
mc.addRow(new Object[]{connected, pebbleKit, pebbleKit, 3, 8, 2, fwString});
return mc;
} else {
return null;
}
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}

View File

@ -0,0 +1,220 @@
package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.database.schema.ActivityDBCreationScript;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.DATABASE_NAME;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandler {
private static final Logger LOG = LoggerFactory.getLogger(ActivityDatabaseHandler.class);
private static final int DATABASE_VERSION = 5;
public ActivityDatabaseHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
try {
ActivityDBCreationScript script = new ActivityDBCreationScript();
script.createSchema(db);
} catch (RuntimeException ex) {
GB.toast("Error creating database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
try {
for (int i = oldVersion + 1; i <= newVersion; i++) {
DBUpdateScript updater = getUpdateScript(db, i);
if (updater != null) {
LOG.info("upgrading activity database to version " + i);
updater.upgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject upgrade
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
try {
for (int i = oldVersion; i >= newVersion; i--) {
DBUpdateScript updater = getUpdateScript(db, i);
if (updater != null) {
LOG.info("downgrading activity database to version " + (i - 1));
updater.downgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject downgrade
}
}
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
try {
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".schema.ActivityDBUpdate_" + version);
return (DBUpdateScript) updateClass.newInstance();
} catch (ClassNotFoundException e) {
return null;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
}
}
public void addGBActivitySample(ActivitySample sample) {
try (SQLiteDatabase db = this.getWritableDatabase()) {
ContentValues values = new ContentValues();
values.put(KEY_TIMESTAMP, sample.getTimestamp());
values.put(KEY_PROVIDER, sample.getProvider().getID());
values.put(KEY_INTENSITY, sample.getRawIntensity());
values.put(KEY_STEPS, sample.getSteps());
values.put(KEY_TYPE, sample.getRawKind());
db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
}
}
/**
* Adds the a new sample to the database
*
* @param timestamp the timestamp of the same, second-based!
* @param provider the SampleProvider ID
* @param intensity the sample's raw intensity value
* @param steps the sample's steps value
* @param kind the raw activity kind of the sample
*/
@Override
public void addGBActivitySample(int timestamp, byte provider, short intensity, short steps, byte kind) {
if (intensity < 0) {
LOG.error("negative intensity received, ignoring");
intensity = 0;
}
if (steps < 0) {
LOG.error("negative steps received, ignoring");
steps = 0;
}
try (SQLiteDatabase db = this.getWritableDatabase()) {
ContentValues values = new ContentValues();
values.put(KEY_TIMESTAMP, timestamp);
values.put(KEY_PROVIDER, provider);
values.put(KEY_INTENSITY, intensity);
values.put(KEY_STEPS, steps);
values.put(KEY_TYPE, kind);
db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
}
}
public ArrayList<ActivitySample> getSleepSamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP, provider);
}
public ArrayList<ActivitySample> getActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY, provider);
}
@Override
public SQLiteOpenHelper getHelper() {
return this;
}
@Override
public void release() {
GBApplication.releaseDB();
}
public ArrayList<ActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL, provider);
}
/**
* Returns all available activity samples from between the two timestamps (inclusive), of the given
* provided and type(s).
*
* @param timestamp_from
* @param timestamp_to
* @param activityTypes ORed combination of #TYPE_DEEP_SLEEP, #TYPE_LIGHT_SLEEP, #TYPE_ACTIVITY
* @param provider the producer of the samples to be sought
* @return
*/
private ArrayList<ActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityTypes, SampleProvider provider) {
if (timestamp_to < 0) {
throw new IllegalArgumentException("negative timestamp_to");
}
if (timestamp_from < 0) {
throw new IllegalArgumentException("negative timestamp_from");
}
ArrayList<ActivitySample> samples = new ArrayList<ActivitySample>();
final String where = "(provider=" + provider.getID() + " and timestamp>=" + timestamp_from + " and timestamp<=" + timestamp_to + getWhereClauseFor(activityTypes, provider) + ")";
LOG.info("Activity query where: "+ where);
final String order = "timestamp";
try (SQLiteDatabase db = this.getReadableDatabase()) {
try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) {
LOG.info("Activity query result: " + cursor.getCount() + " samples");
if (cursor.moveToFirst()) {
do {
GBActivitySample sample = new GBActivitySample(
provider,
cursor.getInt(cursor.getColumnIndex(KEY_TIMESTAMP)),
cursor.getShort(cursor.getColumnIndex(KEY_INTENSITY)),
cursor.getShort(cursor.getColumnIndex(KEY_STEPS)),
(byte) cursor.getShort(cursor.getColumnIndex(KEY_TYPE)));
samples.add(sample);
} while (cursor.moveToNext());
}
}
}
return samples;
}
private String getWhereClauseFor(int activityTypes, SampleProvider provider) {
if (activityTypes == ActivityKind.TYPE_ALL) {
return ""; // no further restriction
}
StringBuilder builder = new StringBuilder(" and (");
byte[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, provider);
for (int i = 0; i < dbActivityTypes.length; i++) {
builder.append(" type=").append(dbActivityTypes[i]);
if (i + 1 < dbActivityTypes.length) {
builder.append(" or ");
}
}
builder.append(')');
return builder.toString();
}
}

View File

@ -1,28 +1,12 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.Context;
import android.os.AsyncTask;
import android.widget.Toast;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class DBAccess extends AsyncTask {
private final String mTask;
@ -42,10 +26,16 @@ public abstract class DBAccess extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
try (DBHandler db = GBApplication.acquireDB()) {
doInBackground(db);
DBHandler handler = null;
try {
handler = GBApplication.acquireDB();
doInBackground(handler);
} catch (Exception e) {
mError = e;
} finally {
if (handler != null) {
handler.release();
}
}
return null;
}

View File

@ -0,0 +1,14 @@
package nodomain.freeyourgadget.gadgetbridge.database;
public class DBConstants {
public static final String DATABASE_NAME = "ActivityDatabase";
public static final String TABLE_GBACTIVITYSAMPLES = "GBActivitySamples";
public static final String TABLE_STEPS_PER_DAY = "StepsPerDay";
public static final String KEY_TIMESTAMP = "timestamp";
public static final String KEY_PROVIDER = "provider";
public static final String KEY_INTENSITY = "intensity";
public static final String KEY_STEPS = "steps";
public static final String KEY_TYPE = "type";
}

View File

@ -1,53 +1,30 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, JohnnySun
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.database;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import java.util.List;
/**
* Provides lowlevel access to the database.
*/
public interface DBHandler extends AutoCloseable {
/**
* Closes the database.
*/
void closeDb();
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
public interface DBHandler {
public SQLiteOpenHelper getHelper();
/**
* Opens the database. Note that this is only possible after an explicit
* #closeDb(). Initially the db is implicitly open.
* Releases the DB handler. No access may be performed after calling this method.
* Same as calling {@link GBApplication#releaseDB()}
*/
void openDb();
void release();
SQLiteOpenHelper getHelper();
List<ActivitySample> getAllActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
/**
* Releases the DB handler. No DB access will be possible before
* #openDb() will be called.
*/
void close() throws Exception;
List<ActivitySample> getActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
SQLiteDatabase getDatabase();
List<ActivitySample> getSleepSamples(int tsFrom, int tsTo, SampleProvider provider);
DaoMaster getDaoMaster();
DaoSession getDaoSession();
void addGBActivitySample(int timestamp, byte provider, short intensity, short steps, byte kind);
SQLiteDatabase getWritableDatabase();
}

View File

@ -1,135 +1,60 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, JohnnySun
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.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.Query;
import de.greenrobot.dao.query.QueryBuilder;
import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescription;
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescriptionDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Tag;
import nodomain.freeyourgadget.gadgetbridge.entities.TagDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.UserAttributes;
import nodomain.freeyourgadget.gadgetbridge.entities.UserDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
/**
* Provides utiliy access to some common entities, so you won't need to use
* their DAO classes.
* <p/>
* Maybe this code should actually be in the DAO classes themselves, but then
* these should be under revision control instead of 100% generated at build time.
*/
public class DBHelper {
private static final Logger LOG = LoggerFactory.getLogger(DBHelper.class);
private final Context context;
public DBHelper(Context context) {
this.context = context;
}
/**
* Closes the database and returns its name.
* Important: after calling this, you have to DBHandler#openDb() it again
* to get it back to work.
*
* @param dbHandler
* @return
* @throws IllegalStateException
*/
private String getClosedDBPath(DBHandler dbHandler) throws IllegalStateException {
SQLiteDatabase db = dbHandler.getDatabase();
private String getClosedDBPath(SQLiteOpenHelper dbHandler) throws IllegalStateException {
SQLiteDatabase db = dbHandler.getReadableDatabase();
String path = db.getPath();
dbHandler.closeDb();
db.close();
if (db.isOpen()) { // reference counted, so may still be open
throw new IllegalStateException("Database must be closed");
}
return path;
}
public File exportDB(DBHandler dbHandler, File toDir) throws IllegalStateException, IOException {
public File exportDB(SQLiteOpenHelper dbHandler, File toDir) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
try {
File sourceFile = new File(dbPath);
File destFile = new File(toDir, sourceFile.getName());
if (destFile.exists()) {
File backup = new File(toDir, destFile.getName() + "_" + getDate());
destFile.renameTo(backup);
} else if (!toDir.exists()) {
if (!toDir.mkdirs()) {
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
}
File sourceFile = new File(dbPath);
File destFile = new File(toDir, sourceFile.getName());
if (destFile.exists()) {
File backup = new File(toDir, destFile.getName() + "_" + getDate());
destFile.renameTo(backup);
} else if (!toDir.exists()) {
if (!toDir.mkdirs()) {
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
}
FileUtils.copyFile(sourceFile, destFile);
return destFile;
} finally {
dbHandler.openDb();
}
FileUtils.copyFile(sourceFile, destFile);
return destFile;
}
private String getDate() {
return new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
}
public void importDB(DBHandler dbHandler, File fromFile) throws IllegalStateException, IOException {
public void importDB(SQLiteOpenHelper dbHandler, File fromFile) throws IllegalStateException, IOException {
String dbPath = getClosedDBPath(dbHandler);
try {
File toFile = new File(dbPath);
FileUtils.copyFile(fromFile, toFile);
} finally {
dbHandler.openDb();
}
File toFile = new File(dbPath);
FileUtils.copyFile(fromFile, toFile);
}
public void validateDB(SQLiteOpenHelper dbHandler) throws IOException {
@ -145,416 +70,16 @@ public class DBHelper {
db.execSQL(statement);
}
public boolean existsDB(String dbName) {
File path = context.getDatabasePath(dbName);
return path != null && path.exists();
}
public static boolean existsColumn(String tableName, String columnName, SQLiteDatabase db) {
try (Cursor res = db.rawQuery("PRAGMA table_info('" + tableName + "')", null)) {
int index = res.getColumnIndex("name");
if (index < 1) {
return false; // something's really wrong
}
while (res.moveToNext()) {
String cn = res.getString(index);
if (columnName.equals(cn)) {
return true;
}
}
}
return false;
}
/**
* WITHOUT ROWID is only available with sqlite 3.8.2, which is available
* with Lollipop and later.
*
* @return the "WITHOUT ROWID" string or an empty string for pre-Lollipop devices
*/
@NonNull
public static String getWithoutRowId() {
if (GBApplication.isRunningLollipopOrLater()) {
return " WITHOUT ROWID;";
}
return "";
}
/**
* Looks up the user entity in the database. If a user exists already, it will
* be updated with the current preferences values. If no user exists yet, it will
* be created in the database.
*
* Note: so far there is only ever a single user; there is no multi-user support yet
* @param session
* @return the User entity
*/
@NonNull
public static User getUser(DaoSession session) {
ActivityUser prefsUser = new ActivityUser();
UserDao userDao = session.getUserDao();
User user;
List<User> users = userDao.loadAll();
if (users.isEmpty()) {
user = createUser(prefsUser, session);
} else {
user = users.get(0); // TODO: multiple users support?
ensureUserUpToDate(user, prefsUser, session);
}
ensureUserAttributes(user, prefsUser, session);
return user;
}
@NonNull
public static UserAttributes getUserAttributes(User user) {
List<UserAttributes> list = user.getUserAttributesList();
if (list.isEmpty()) {
throw new IllegalStateException("user has no attributes");
}
return list.get(0);
}
@NonNull
private static User createUser(ActivityUser prefsUser, DaoSession session) {
User user = new User();
ensureUserUpToDate(user, prefsUser, session);
return user;
}
private static void ensureUserUpToDate(User user, ActivityUser prefsUser, DaoSession session) {
if (!isUserUpToDate(user, prefsUser)) {
user.setName(prefsUser.getName());
user.setBirthday(prefsUser.getUserBirthday());
user.setGender(prefsUser.getGender());
if (user.getId() == null) {
session.getUserDao().insert(user);
} else {
session.getUserDao().update(user);
}
}
}
public static boolean isUserUpToDate(User user, ActivityUser prefsUser) {
if (!Objects.equals(user.getName(), prefsUser.getName())) {
return false;
}
if (!Objects.equals(user.getBirthday(), prefsUser.getUserBirthday())) {
return false;
}
if (user.getGender() != prefsUser.getGender()) {
return false;
}
return true;
}
private static void ensureUserAttributes(User user, ActivityUser prefsUser, DaoSession session) {
List<UserAttributes> userAttributes = user.getUserAttributesList();
UserAttributes[] previousUserAttributes = new UserAttributes[1];
if (hasUpToDateUserAttributes(userAttributes, prefsUser, previousUserAttributes)) {
return;
}
Calendar now = DateTimeUtils.getCalendarUTC();
invalidateUserAttributes(previousUserAttributes[0], now, session);
UserAttributes attributes = new UserAttributes();
attributes.setValidFromUTC(now.getTime());
attributes.setHeightCM(prefsUser.getHeightCm());
attributes.setWeightKG(prefsUser.getWeightKg());
attributes.setSleepGoalHPD(prefsUser.getSleepDuration());
attributes.setStepsGoalSPD(prefsUser.getStepsGoal());
attributes.setUserId(user.getId());
session.getUserAttributesDao().insert(attributes);
// sort order is important, so we re-fetch from the db
// userAttributes.add(attributes);
user.resetUserAttributesList();
}
private static void invalidateUserAttributes(UserAttributes userAttributes, Calendar now, DaoSession session) {
if (userAttributes != null) {
Calendar invalid = (Calendar) now.clone();
invalid.add(Calendar.MINUTE, -1);
userAttributes.setValidToUTC(invalid.getTime());
session.getUserAttributesDao().update(userAttributes);
}
}
private static boolean hasUpToDateUserAttributes(List<UserAttributes> userAttributes, ActivityUser prefsUser, UserAttributes[] outPreviousUserAttributes) {
for (UserAttributes attr : userAttributes) {
if (!isValidNow(attr)) {
continue;
}
if (isEqual(attr, prefsUser)) {
return true;
} else {
outPreviousUserAttributes[0] = attr;
}
}
return false;
}
// TODO: move this into db queries?
private static boolean isValidNow(ValidByDate element) {
Calendar cal = DateTimeUtils.getCalendarUTC();
Date nowUTC = cal.getTime();
return isValid(element, nowUTC);
}
private static boolean isValid(ValidByDate element, Date nowUTC) {
Date validFromUTC = element.getValidFromUTC();
Date validToUTC = element.getValidToUTC();
if (nowUTC.before(validFromUTC)) {
return false;
}
if (validToUTC != null && nowUTC.after(validToUTC)) {
return false;
}
return true;
}
private static boolean isEqual(UserAttributes attr, ActivityUser prefsUser) {
if (prefsUser.getHeightCm() != attr.getHeightCM()) {
LOG.info("user height changed to " + prefsUser.getHeightCm() + " from " + attr.getHeightCM());
return false;
}
if (prefsUser.getWeightKg() != attr.getWeightKG()) {
LOG.info("user changed to " + prefsUser.getWeightKg() + " from " + attr.getWeightKG());
return false;
}
if (!Integer.valueOf(prefsUser.getSleepDuration()).equals(attr.getSleepGoalHPD())) {
LOG.info("user sleep goal changed to " + prefsUser.getSleepDuration() + " from " + attr.getSleepGoalHPD());
return false;
}
if (!Integer.valueOf(prefsUser.getStepsGoal()).equals(attr.getStepsGoalSPD())) {
LOG.info("user steps goal changed to " + prefsUser.getStepsGoal() + " from " + attr.getStepsGoalSPD());
return false;
}
return true;
}
private static boolean isEqual(DeviceAttributes attr, GBDevice gbDevice) {
if (!Objects.equals(attr.getFirmwareVersion1(), gbDevice.getFirmwareVersion())) {
return false;
}
if (!Objects.equals(attr.getFirmwareVersion2(), gbDevice.getFirmwareVersion2())) {
return false;
}
if (!Objects.equals(attr.getVolatileIdentifier(), gbDevice.getVolatileAddress())) {
return false;
}
return true;
}
public static Device findDevice(GBDevice gbDevice, DaoSession session) {
DeviceDao deviceDao = session.getDeviceDao();
Query<Device> query = deviceDao.queryBuilder().where(DeviceDao.Properties.Identifier.eq(gbDevice.getAddress())).build();
List<Device> devices = query.list();
if (devices.size() > 0) {
return devices.get(0);
}
return null;
}
/**
* Returns all active (that is, not old, archived ones) from the database.
* (currently the active handling is not available)
* @param daoSession
*/
public static List<Device> getActiveDevices(DaoSession daoSession) {
return daoSession.getDeviceDao().loadAll();
}
/**
* Looks up in the database the Device entity corresponding to the GBDevice. If a device
* exists already, it will be updated with the current preferences values. If no device exists
* yet, it will be created in the database.
*
* @param session
* @return the device entity corresponding to the given GBDevice
*/
public static Device getDevice(GBDevice gbDevice, DaoSession session) {
Device device = findDevice(gbDevice, session);
if (device == null) {
device = createDevice(gbDevice, session);
} else {
ensureDeviceUpToDate(device, gbDevice, session);
}
if (gbDevice.isInitialized()) {
ensureDeviceAttributes(device, gbDevice, session);
}
return device;
}
@NonNull
public static DeviceAttributes getDeviceAttributes(Device device) {
List<DeviceAttributes> list = device.getDeviceAttributesList();
if (list.isEmpty()) {
throw new IllegalStateException("device has no attributes");
}
return list.get(0);
}
private static void ensureDeviceUpToDate(Device device, GBDevice gbDevice, DaoSession session) {
if (!isDeviceUpToDate(device, gbDevice)) {
device.setIdentifier(gbDevice.getAddress());
device.setName(gbDevice.getName());
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
device.setManufacturer(coordinator.getManufacturer());
device.setType(gbDevice.getType().getKey());
device.setModel(gbDevice.getModel());
if (device.getId() == null) {
session.getDeviceDao().insert(device);
} else {
session.getDeviceDao().update(device);
}
}
}
private static boolean isDeviceUpToDate(Device device, GBDevice gbDevice) {
if (!Objects.equals(device.getIdentifier(), gbDevice.getAddress())) {
return false;
}
if (!Objects.equals(device.getName(), gbDevice.getName())) {
return false;
}
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
if (!Objects.equals(device.getManufacturer(), coordinator.getManufacturer())) {
return false;
}
if (device.getType() != gbDevice.getType().getKey()) {
return false;
}
if (!Objects.equals(device.getModel(), gbDevice.getModel())) {
return false;
}
return true;
}
private static Device createDevice(GBDevice gbDevice, DaoSession session) {
Device device = new Device();
ensureDeviceUpToDate(device, gbDevice, session);
return device;
}
private static void ensureDeviceAttributes(Device device, GBDevice gbDevice, DaoSession session) {
List<DeviceAttributes> deviceAttributes = device.getDeviceAttributesList();
DeviceAttributes[] previousDeviceAttributes = new DeviceAttributes[1];
if (hasUpToDateDeviceAttributes(deviceAttributes, gbDevice, previousDeviceAttributes)) {
return;
}
Calendar now = DateTimeUtils.getCalendarUTC();
invalidateDeviceAttributes(previousDeviceAttributes[0], now, session);
DeviceAttributes attributes = new DeviceAttributes();
attributes.setDeviceId(device.getId());
attributes.setValidFromUTC(now.getTime());
attributes.setFirmwareVersion1(gbDevice.getFirmwareVersion());
attributes.setFirmwareVersion2(gbDevice.getFirmwareVersion2());
attributes.setVolatileIdentifier(gbDevice.getVolatileAddress());
DeviceAttributesDao attributesDao = session.getDeviceAttributesDao();
attributesDao.insert(attributes);
// sort order is important, so we re-fetch from the db
// deviceAttributes.add(attributes);
device.resetDeviceAttributesList();
}
private static void invalidateDeviceAttributes(DeviceAttributes deviceAttributes, Calendar now, DaoSession session) {
if (deviceAttributes != null) {
Calendar invalid = (Calendar) now.clone();
invalid.add(Calendar.MINUTE, -1);
deviceAttributes.setValidToUTC(invalid.getTime());
session.getDeviceAttributesDao().update(deviceAttributes);
}
}
private static boolean hasUpToDateDeviceAttributes(List<DeviceAttributes> deviceAttributes, GBDevice gbDevice, DeviceAttributes[] outPreviousAttributes) {
for (DeviceAttributes attr : deviceAttributes) {
if (!isValidNow(attr)) {
continue;
}
if (isEqual(attr, gbDevice)) {
return true;
} else {
outPreviousAttributes[0] = attr;
}
}
return false;
}
@NonNull
public static List<ActivityDescription> findActivityDecriptions(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
Property tsFromProperty = ActivityDescriptionDao.Properties.TimestampFrom;
Property tsToProperty = ActivityDescriptionDao.Properties.TimestampTo;
Property userIdProperty = ActivityDescriptionDao.Properties.UserId;
QueryBuilder<ActivityDescription> qb = session.getActivityDescriptionDao().queryBuilder();
qb.where(userIdProperty.eq(user.getId()), isAtLeastPartiallyInRange(qb, tsFromProperty, tsToProperty, tsFrom, tsTo));
List<ActivityDescription> descriptions = qb.build().list();
return descriptions;
}
/**
* Returns a condition that matches when the range of the entity (tsFromProperty..tsToProperty)
* is completely or partially inside the range tsFrom..tsTo.
* @param qb the query builder to use
* @param tsFromProperty the property indicating the start of the entity's range
* @param tsToProperty the property indicating the end of the entity's range
* @param tsFrom the timestamp indicating the start of the range to match
* @param tsTo the timestamp indicating the end of the range to match
* @param <T> the query builder's type parameter
* @return the range WhereCondition
*/
private static <T> WhereCondition isAtLeastPartiallyInRange(QueryBuilder<T> qb, Property tsFromProperty, Property tsToProperty, int tsFrom, int tsTo) {
return qb.and(tsFromProperty.lt(tsTo), tsToProperty.gt(tsFrom));
}
@NonNull
public static ActivityDescription createActivityDescription(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
ActivityDescription desc = new ActivityDescription();
desc.setUser(user);
desc.setTimestampFrom(tsFrom);
desc.setTimestampTo(tsTo);
session.getActivityDescriptionDao().insertOrReplace(desc);
return desc;
}
@NonNull
public static Tag getTag(@NonNull User user, @NonNull String name, @NonNull DaoSession session) {
TagDao tagDao = session.getTagDao();
QueryBuilder<Tag> qb = tagDao.queryBuilder();
Query<Tag> query = qb.where(TagDao.Properties.UserId.eq(user.getId()), TagDao.Properties.Name.eq(name)).build();
List<Tag> tags = query.list();
if (tags.size() > 0) {
return tags.get(0);
}
return createTag(user, name, null, session);
}
static Tag createTag(@NonNull User user, @NonNull String name, @Nullable String description, @NonNull DaoSession session) {
Tag tag = new Tag();
tag.setUserId(user.getId());
tag.setName(name);
tag.setDescription(description);
session.getTagDao().insertOrReplace(tag);
return tag;
}
public static void clearSession() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
session.clear();
} catch (Exception e) {
LOG.warn("Unable to acquire database to clear the session", e);
}
}
}

View File

@ -1,50 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.schema.SchemaMigration;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
public class DBOpenHelper extends DaoMaster.OpenHelper {
private final String updaterClassNamePrefix;
private final Context context;
public DBOpenHelper(Context context, String dbName, SQLiteDatabase.CursorFactory factory) {
super(context, dbName, factory);
updaterClassNamePrefix = dbName + "Update_";
this.context = context;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
DaoMaster.createAllTables(db, true);
new SchemaMigration(updaterClassNamePrefix).onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
DaoMaster.createAllTables(db, true);
new SchemaMigration(updaterClassNamePrefix).onDowngrade(db, oldVersion, newVersion);
}
public Context getContext() {
return context;
}
}

View File

@ -1,19 +1,3 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database;
import android.database.sqlite.SQLiteDatabase;

View File

@ -1,19 +1,3 @@
/* Copyright (C) 2015-2017 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.database;
/**

View File

@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
public class ActivityDBCreationScript {
public void createSchema(SQLiteDatabase db) {
String CREATE_GBACTIVITYSAMPLES_TABLE = "CREATE TABLE " + TABLE_GBACTIVITYSAMPLES + " ("
+ KEY_TIMESTAMP + " INT,"
+ KEY_PROVIDER + " TINYINT,"
+ KEY_INTENSITY + " SMALLINT,"
+ KEY_STEPS + " TINYINT,"
+ KEY_TYPE + " TINYINT,"
+ " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId();
db.execSQL(CREATE_GBACTIVITYSAMPLES_TABLE);
}
}

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
/**
* Upgrade and downgrade with DB versions <= 5 is not supported.
* Just recreates the default schema. Those GB versions may or may not
* work with that, but this code will probably not create a DB for them
* anyway.
*/
public class ActivityDBUpdate_4 extends ActivityDBCreationScript implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
recreateSchema(db);
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
recreateSchema(db);
}
private void recreateSchema(SQLiteDatabase db) {
DBHelper.dropTable(TABLE_GBACTIVITYSAMPLES, db);
createSchema(db);
}
}

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_STEPS_PER_DAY;
/**
* Adds a table "STEPS_PER_DAY".
*/
public class ActivityDBUpdate_6 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
String CREATE_STEPS_PER_DAY_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_STEPS_PER_DAY + " ("
+ KEY_TIMESTAMP + " INT,"
+ KEY_PROVIDER + " TINYINT,"
+ KEY_STEPS + " MEDIUMINT,"
+ " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId();
db.execSQL(CREATE_STEPS_PER_DAY_TABLE);
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
DBHelper.dropTable(TABLE_STEPS_PER_DAY, db);
}
}

View File

@ -1,42 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
/*
* adds heart rate column to health table
*/
public class GadgetbridgeUpdate_14 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(PebbleHealthActivitySampleDao.TABLENAME, PebbleHealthActivitySampleDao.Properties.HeartRate.columnName, db)) {
String ADD_COLUMN_HEART_RATE = "ALTER TABLE " + PebbleHealthActivitySampleDao.TABLENAME + " ADD COLUMN "
+ PebbleHealthActivitySampleDao.Properties.HeartRate.columnName + " INTEGER NOT NULL DEFAULT 0;";
db.execSQL(ADD_COLUMN_HEART_RATE);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -1,42 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
/*
* adds heart rate column to health table
*/
public class GadgetbridgeUpdate_15 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(DeviceAttributesDao.TABLENAME, DeviceAttributesDao.Properties.VolatileIdentifier.columnName, db)) {
String ADD_COLUMN_VOLATILE_IDENTIFIER = "ALTER TABLE " + DeviceAttributesDao.TABLENAME + " ADD COLUMN "
+ DeviceAttributesDao.Properties.VolatileIdentifier.columnName + " TEXT;";
db.execSQL(ADD_COLUMN_VOLATILE_IDENTIFIER);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -1,38 +0,0 @@
/* Copyright (C) 2017 protomors
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.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.No1F1ActivitySampleDao;
public class GadgetbridgeUpdate_17 implements DBUpdateScript {
@Override
public void upgradeSchema(SQLiteDatabase db) {
if (!DBHelper.existsColumn(No1F1ActivitySampleDao.TABLENAME, No1F1ActivitySampleDao.Properties.HeartRate.columnName, db)) {
String ADD_COLUMN_HEART_RATE = "ALTER TABLE " + No1F1ActivitySampleDao.TABLENAME + " ADD COLUMN "
+ No1F1ActivitySampleDao.Properties.HeartRate.columnName + " INTEGER NOT NULL DEFAULT 0;";
db.execSQL(ADD_COLUMN_HEART_RATE);
}
}
@Override
public void downgradeSchema(SQLiteDatabase db) {
}
}

View File

@ -1,80 +0,0 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class SchemaMigration {
private static final Logger LOG = LoggerFactory.getLogger(SchemaMigration.class);
private final String classNamePrefix;
public SchemaMigration(String updaterClassNamePrefix) {
classNamePrefix = updaterClassNamePrefix;
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LOG.info("ActivityDatabase: schema upgrade requested from " + oldVersion + " to " + newVersion);
try {
for (int i = oldVersion + 1; i <= newVersion; i++) {
DBUpdateScript updater = getUpdateScript(db, i);
if (updater != null) {
LOG.info("upgrading activity database to version " + i);
updater.upgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject upgrade
}
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LOG.info("ActivityDatabase: schema downgrade requested from " + oldVersion + " to " + newVersion);
try {
for (int i = oldVersion; i >= newVersion; i--) {
DBUpdateScript updater = getUpdateScript(db, i);
if (updater != null) {
LOG.info("downgrading activity database to version " + (i - 1));
updater.downgradeSchema(db);
}
}
LOG.info("activity database is now at version " + newVersion);
} catch (RuntimeException ex) {
GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
throw ex; // reject downgrade
}
}
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
try {
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + "." + classNamePrefix + version);
return (DBUpdateScript) updateClass.newInstance();
} catch (ClassNotFoundException e) {
return null;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
}
}
}

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