mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-12 13:09:24 +01:00
Merge branch 'master' into new_GUI
This commit is contained in:
commit
d12103e95d
99
CHANGELOG.md
99
CHANGELOG.md
@ -1,5 +1,52 @@
|
||||
###Changelog
|
||||
|
||||
####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
|
||||
@ -25,7 +72,7 @@
|
||||
* 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 ordert to support data sharing with Mi Fit (#250)
|
||||
* 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
|
||||
|
||||
@ -56,7 +103,7 @@
|
||||
|
||||
####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 notfications since upstream dropped special pebble support
|
||||
* 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!
|
||||
@ -73,13 +120,13 @@
|
||||
####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 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: 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)
|
||||
* 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)
|
||||
|
||||
@ -92,13 +139,13 @@
|
||||
####Version 0.10.2
|
||||
* Pebble: allow to manually paste configuration data for legacy configuration pages
|
||||
* Pebble: various improvements to the configuration page
|
||||
* Pebble: Suppport FW 4.0-dp1 and Pebble2 emulator (needs recompilation of Gadgetbridge)
|
||||
* 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 improvemnts to music playback
|
||||
* Pebble: allow ignoring activity trackers indiviually (to keep the data on the pebble)
|
||||
* 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
|
||||
@ -107,12 +154,12 @@
|
||||
* 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 whithout restarting Gadgetbridge
|
||||
* 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 reconnnect issues
|
||||
* 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
|
||||
@ -124,7 +171,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: inital experimental and probably broken support for Amazfit
|
||||
* 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
|
||||
|
||||
@ -206,7 +253,7 @@
|
||||
####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. Insigths are NOT activated.
|
||||
* 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
|
||||
@ -215,7 +262,7 @@
|
||||
|
||||
####Version 0.7.2
|
||||
* Pebble: Allow replying to generic notifications that contain a wearable reply action (tested with Signal)
|
||||
* Pebble: Support seting up a common suffix for canned replies (defaults to " (canned reply)")
|
||||
* 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
|
||||
@ -232,7 +279,7 @@
|
||||
* 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
|
||||
* 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
|
||||
@ -256,7 +303,7 @@
|
||||
* 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 pemissions)
|
||||
* 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)
|
||||
@ -290,7 +337,7 @@
|
||||
* Try to prevent service being killed by disallowing backups
|
||||
|
||||
####Version 0.6.2
|
||||
* Mi Band: support firmare versione 1.0.10.14 (and onwards?) vibration
|
||||
* 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
|
||||
|
||||
@ -302,11 +349,11 @@
|
||||
* Bugfix for app blacklist (some checkboxes where wrongly drawn as checked)
|
||||
|
||||
####Version 0.6.0
|
||||
* Pebble: WIP implementantion of PebbleKit Intents to make some 3rd party Android apps work with the Pebble (eg. Ventoo)
|
||||
* 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 contolling all audio players that react to media buttons (can be chosen in settings)
|
||||
* 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 messagess as chat messages, even if arrived via Pebble Intents (nice icon for Pebble FW 3.x)
|
||||
* 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
|
||||
|
||||
@ -330,7 +377,7 @@
|
||||
* 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 (Marshmellow)
|
||||
* Build target is now Android SDK 23 (Marshmallow)
|
||||
|
||||
####Version 0.5.1
|
||||
* Pebble: support taking screenshot from Pebble Time
|
||||
@ -343,7 +390,7 @@
|
||||
* 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
|
||||
* Mi Band: Fixed negative number of steps displayed (#91)
|
||||
@ -358,13 +405,13 @@
|
||||
####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
|
||||
* 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
|
||||
@ -441,7 +488,7 @@
|
||||
####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
|
||||
@ -464,8 +511,8 @@
|
||||
* 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
|
||||
* 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
|
||||
|
@ -26,26 +26,32 @@ import de.greenrobot.daogenerator.Schema;
|
||||
*/
|
||||
public class GBDaoGenerator {
|
||||
|
||||
public static final String VALID_FROM_UTC = "validFromUTC";
|
||||
public static final String VALID_TO_UTC = "validToUTC";
|
||||
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";
|
||||
public static final String SAMPLE_RAW_INTENSITY = "rawIntensity";
|
||||
public static final String SAMPLE_STEPS = "steps";
|
||||
public static final String SAMPLE_RAW_KIND = "rawKind";
|
||||
public static final String TIMESTAMP_FROM = "timestampFrom";
|
||||
public static final String TIMESTAMP_TO = "timestampTo";
|
||||
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(13, MAIN_PACKAGE + ".entities");
|
||||
Schema schema = new Schema(15, 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);
|
||||
|
||||
@ -172,7 +178,7 @@ public class GBDaoGenerator {
|
||||
}
|
||||
|
||||
private static void addHeartRateProperties(Entity activitySample) {
|
||||
activitySample.addIntProperty("heartRate").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
}
|
||||
|
||||
private static Entity addPebbleHealthActivitySample(Schema schema, Entity user, Entity device) {
|
||||
@ -181,6 +187,7 @@ public class GBDaoGenerator {
|
||||
activitySample.addByteArrayProperty("rawPebbleHealthData").codeBeforeGetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
|
64
README.md
64
README.md
@ -1,7 +1,7 @@
|
||||
Gadgetbridge
|
||||
============
|
||||
|
||||
Gadgetbridge is an Android (4.4+) Application which will allow you to use your
|
||||
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.
|
||||
|
||||
@ -15,12 +15,11 @@ need to create an account and transmit any of your data to the vendor's servers.
|
||||
|
||||
## Supported Devices
|
||||
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round
|
||||
* Pebble 2, Pebble Time 2 (experimental, PAIR WITHIN GADGETBRIDGE)
|
||||
* Mi Band, Mi Band 1A, Mi Band 1S
|
||||
* Mi Band 2 (only notifications)
|
||||
* Mi Band 2
|
||||
* Vibratissimo (experimental)
|
||||
|
||||
***THE PEBBLE 2 AND PEBBLE TIME 2 ARE CURRENTLY NOT SUPPORTED. ADDING SUPPORT IS HIGH-PRIORITY BUT WE CANNOT GIVE YOU AN ETA!***
|
||||
|
||||
* Liveview
|
||||
|
||||
## Features (Pebble)
|
||||
|
||||
@ -31,7 +30,7 @@ need to create an account and transmit any of your data to the vendor's servers.
|
||||
* K-9 Mail notification support
|
||||
* Support for generic notifications (above filtered out)
|
||||
* Support for up to 16 predefined replies for SMS and Android Wear compatible notifications (experimental, tested with Signal)
|
||||
* Dismiss individial notifications, mute or open corresponding app on phone from the action menu (generic notifications)
|
||||
* Dismiss individual notifications, mute or open corresponding app on phone from the action menu (generic notifications)
|
||||
* Dismiss all notifications from the action menu (non-generic notifications)
|
||||
* Music playback info (artist, album, track)
|
||||
* Music control: play/pause, next track, previous track, volume up, volume down
|
||||
@ -50,44 +49,62 @@ need to create an account and transmit any of your data to the vendor's servers.
|
||||
|
||||
## Getting Started (Pebble)
|
||||
|
||||
1. Pair your Pebble through the Android's Bluetooth Settings
|
||||
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)
|
||||
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)
|
||||
## Features (Mi Band 1x)
|
||||
|
||||
* Discovery and pairing
|
||||
* Mi Band notifications (LEDs + vibration) for
|
||||
* Discovery and pairing
|
||||
* Display live activity data (alpha)
|
||||
* Incoming calls
|
||||
* SMS received
|
||||
* K-9 mails received
|
||||
* Conversations messages
|
||||
* Generic Android notifications
|
||||
* Synchronize the time to the Mi Band
|
||||
* Display firmware version and battery state
|
||||
* Firmware Update
|
||||
* Heartrate Measurement (alpha)
|
||||
* Firmware update
|
||||
* Heart rate measurement on demand and during sleep
|
||||
* Synchronize activity data
|
||||
* Display sleep data (alpha)
|
||||
* Display sports data (step count) (alpha)
|
||||
* Display live activity data (alpha)
|
||||
* Set alarms on the Mi Band
|
||||
|
||||
## How to use (Mi Band)
|
||||
## Features (Mi Band 2)
|
||||
|
||||
* 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
|
||||
* Discovery and pairing
|
||||
* Mi Band notifications (Display + vibration) for
|
||||
* Incoming calls
|
||||
* SMS received
|
||||
* K-9 mails received
|
||||
* Conversations messages
|
||||
* Generic Android notifications
|
||||
* Synchronize the time to the Mi Band 2
|
||||
* Display firmware version
|
||||
* Firmware update (beta)
|
||||
* Heart rate measurement on demand and during sleep
|
||||
* Synchronize activity data (alpha)
|
||||
* Set alarms on the Mi Band 2
|
||||
|
||||
## How to use (Mi Band 1+2)
|
||||
|
||||
* 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
|
||||
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 with all three LEDs during the pairing process,
|
||||
When your Mi Band starts to vibrate and blink 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" item to connect if you're not connected yet.
|
||||
2. Go back to the "Gadgetbridge" activity
|
||||
3. Tap the Mi Band item to connect if you're not connected yet
|
||||
4. To test, chose "Debug" from the menu and play around
|
||||
|
||||
Known Issues:
|
||||
@ -95,6 +112,14 @@ Known Issues:
|
||||
* 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.
|
||||
|
||||
|
||||
## Features (Liveview)
|
||||
|
||||
* set time (automatically upon connection)
|
||||
* display notifications and vibrate
|
||||
|
||||
## Authors (in order of first code contribution)
|
||||
|
||||
@ -114,10 +139,9 @@ Translations can be contributed via https://www.transifex.com/projects/p/gadgetb
|
||||
|
||||
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 questions wasn't already answered. Don't forget to check the **closed** issues as well!
|
||||
- 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.
|
||||
|
||||
|
||||
## Having problems?
|
||||
|
||||
1. Open Gadgetbridge's settings and check the option to write log files
|
||||
|
@ -26,8 +26,8 @@ android {
|
||||
targetSdkVersion 23
|
||||
|
||||
// note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.13.7"
|
||||
versionCode 69
|
||||
versionName "0.15.0"
|
||||
versionCode 77
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
@ -72,7 +72,7 @@ dependencies {
|
||||
compile 'com.android.support:design:23.4.0'
|
||||
compile 'com.github.tony19:logback-android-classic:1.1.1-4'
|
||||
compile 'org.slf4j:slf4j-api:1.7.7'
|
||||
compile 'com.github.PhilJay:MPAndroidChart:v3.0.0'
|
||||
compile 'com.github.PhilJay:MPAndroidChart:v3.0.1'
|
||||
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'
|
||||
|
@ -174,7 +174,7 @@
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- to receive the firmwares from the donwload content provider -->
|
||||
<!-- to receive the firmwares from the download content provider -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -182,7 +182,7 @@
|
||||
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
</intent-filter>
|
||||
<!-- to receive firmwares from the donwload content provider if recognized as zip-->
|
||||
<!-- to receive firmwares from the download content provider if recognized as zip-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -256,6 +256,9 @@
|
||||
<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"
|
||||
|
@ -54,7 +54,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="step1" class="step">
|
||||
<h2>Url of the configuration:</h2>
|
||||
<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()">
|
||||
@ -67,7 +67,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div id="step1compat" class="step">
|
||||
<p>In case of "network error" after saving settings in the watchhapp, copy the "network error"
|
||||
<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
|
||||
|
@ -1,5 +1,20 @@
|
||||
//clay stores the values in the localStorage
|
||||
localStorage.clear();
|
||||
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
|
||||
@ -51,36 +66,37 @@ 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];
|
||||
}
|
||||
|
||||
this.addEventListener = function(e, f) {
|
||||
if(e == 'ready') {
|
||||
self.ready = f;
|
||||
}
|
||||
if(e == 'showConfiguration') {
|
||||
self.showConfiguration = f;
|
||||
}
|
||||
if(e == 'webviewclosed') {
|
||||
self.parseconfig = f;
|
||||
}
|
||||
if(e == 'appmessage') {
|
||||
self.appmessage = f;
|
||||
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.removeEventListener = function(e, f) {
|
||||
if(e == 'ready') {
|
||||
self.ready = null;
|
||||
}
|
||||
if(e == 'showConfiguration') {
|
||||
self.showConfiguration = null;
|
||||
}
|
||||
if(e == 'webviewclosed') {
|
||||
self.parseconfig = null;
|
||||
}
|
||||
if(e == 'appmessage') {
|
||||
self.appmessage = null;
|
||||
}
|
||||
}
|
||||
this.actuallyOpenURL = function() {
|
||||
showStep("step1compat");
|
||||
window.open(self.configurationURL.toString(), "config");
|
||||
@ -150,8 +166,6 @@ function gbPebble() {
|
||||
GBjs.gbLog("app wanted to show: " + title + " body: "+ body);
|
||||
}
|
||||
|
||||
this.ready = function() {
|
||||
}
|
||||
|
||||
this.showConfiguration = function() {
|
||||
console.error("This watchapp doesn't support configuration");
|
||||
@ -164,8 +178,8 @@ function gbPebble() {
|
||||
|
||||
if (str.split(needle)[1] !== undefined) {
|
||||
var t = new Object();
|
||||
t.response = unescape(str.split(needle)[1]);
|
||||
self.parseconfig(t);
|
||||
t.response = decodeURIComponent(str.split(needle)[1]);
|
||||
self.evaluate('webviewclosed',[t]);
|
||||
showStep("step2");
|
||||
} else {
|
||||
console.error("No valid configuration found in the entered string.");
|
||||
@ -181,13 +195,16 @@ var storedPreset = GBjs.getAppStoredPreset();
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (jsConfigFile != null) {
|
||||
loadScript(jsConfigFile, function() {
|
||||
Pebble.evaluate('ready');
|
||||
if (getURLVariable('config') == 'true') {
|
||||
showStep("step2");
|
||||
var json_string = unescape(getURLVariable('json'));
|
||||
var json_string = getURLVariable('json');
|
||||
var t = new Object();
|
||||
t.response = json_string;
|
||||
if (json_string != '')
|
||||
Pebble.parseconfig(t);
|
||||
if (json_string != '') {
|
||||
Pebble.evaluate('webviewclosed',[t]);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (storedPreset === undefined) {
|
||||
var presetElements = document.getElementsByClassName("load_presets");
|
||||
@ -195,8 +212,7 @@ if (jsConfigFile != null) {
|
||||
presetElements[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
Pebble.ready();
|
||||
Pebble.showConfiguration();
|
||||
Pebble.evaluate('showConfiguration');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import android.util.TypedValue;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@ -32,6 +33,7 @@ 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;
|
||||
@ -75,14 +77,14 @@ public class GBApplication extends Application {
|
||||
return dir.getAbsolutePath();
|
||||
}
|
||||
};
|
||||
private static DeviceManager deviceManager;
|
||||
|
||||
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();
|
||||
GB.removeAllNotifications(context);
|
||||
}
|
||||
|
||||
public GBApplication() {
|
||||
@ -131,6 +133,31 @@ public class GBApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
@ -173,7 +200,7 @@ 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.
|
||||
*
|
||||
@ -241,7 +268,7 @@ public class GBApplication extends Application {
|
||||
@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 (priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
|
||||
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) == Policy.PRIORITY_CATEGORY_MESSAGES) {
|
||||
return isPrioritySender(notificationPolicy.priorityMessageSenders, number);
|
||||
}
|
||||
@ -393,6 +420,7 @@ public class GBApplication extends Application {
|
||||
theme.resolveAttribute(android.R.attr.textColor, typedValue, true);
|
||||
return typedValue.data;
|
||||
}
|
||||
|
||||
public static int getBackgroundColor(Context context) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
@ -408,7 +436,7 @@ public class GBApplication extends Application {
|
||||
return gbPrefs;
|
||||
}
|
||||
|
||||
public static DeviceManager getDeviceManager() {
|
||||
public DeviceManager getDeviceManager() {
|
||||
return deviceManager;
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,8 @@ public class GBEnvironment {
|
||||
return env;
|
||||
}
|
||||
|
||||
public static GBEnvironment createDeviceEnvironment() {
|
||||
GBEnvironment env = new GBEnvironment();
|
||||
return env;
|
||||
static GBEnvironment createDeviceEnvironment() {
|
||||
return new GBEnvironment();
|
||||
}
|
||||
|
||||
public final boolean isTest() {
|
||||
|
@ -126,6 +126,18 @@ public abstract class Logging {
|
||||
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) {
|
||||
|
@ -98,7 +98,7 @@ public class ControlCenter extends GBActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_controlcenter);
|
||||
|
||||
deviceManager = GBApplication.getDeviceManager();
|
||||
deviceManager = ((GBApplication)getApplication()).getDeviceManager();
|
||||
|
||||
hintTextView = (TextView) findViewById(R.id.hintTextView);
|
||||
ListView deviceListView = (ListView) findViewById(R.id.deviceListView);
|
||||
|
@ -190,7 +190,7 @@ public class DbManagementActivity extends GBActivity {
|
||||
}
|
||||
|
||||
private void selectDeviceForMergingActivityDatabaseInto(final DeviceSelectionCallback callback) {
|
||||
GBDevice connectedDevice = GBApplication.getDeviceManager().getSelectedDevice();
|
||||
GBDevice connectedDevice = ((GBApplication)getApplication()).getDeviceManager().getSelectedDevice();
|
||||
if (connectedDevice == null) {
|
||||
callback.invoke(null);
|
||||
return;
|
||||
|
@ -7,6 +7,8 @@ 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;
|
||||
@ -32,6 +34,7 @@ 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;
|
||||
@ -39,6 +42,8 @@ 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;
|
||||
|
||||
@ -91,6 +96,14 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
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 && device.getAddress().equals(bondingAddress)) {
|
||||
@ -115,10 +128,10 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
};
|
||||
|
||||
|
||||
// why use a method to to get callback?
|
||||
// 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 woth SDK check to return this callback
|
||||
// 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() {
|
||||
@ -127,10 +140,18 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
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() + ": " +
|
||||
((result.getScanRecord() != null) ? result.getScanRecord().getBytes().length : -1));
|
||||
//logMessageContent(result.getScanRecord().getBytes());
|
||||
handleDeviceFound(result.getDevice(), (short) result.getRssi());
|
||||
((scanRecord != null) ? scanRecord.getBytes().length : -1));
|
||||
handleDeviceFound(result.getDevice(), (short) result.getRssi(), uuids);
|
||||
} catch (NullPointerException e) {
|
||||
LOG.warn("Error handling scan result", e);
|
||||
}
|
||||
@ -195,6 +216,7 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
|
||||
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);
|
||||
@ -243,9 +265,20 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
}
|
||||
|
||||
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()) {
|
||||
ParcelUuid[] uuids = device.getUuids();
|
||||
if (uuids != null && uuids.length > 0) {
|
||||
for (ParcelUuid uuid : uuids) {
|
||||
LOG.debug(" supports uuid: " + uuid.toString());
|
||||
@ -256,8 +289,10 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
return; // ignore already bonded devices
|
||||
}
|
||||
|
||||
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi);
|
||||
if (DeviceHelper.getInstance().isSupported(candidate)) {
|
||||
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
|
||||
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
|
||||
if (deviceType.isSupported()) {
|
||||
candidate.setDeviceType(deviceType);
|
||||
int index = deviceCandidates.indexOf(candidate);
|
||||
if (index >= 0) {
|
||||
deviceCandidates.set(index, candidate); // replace
|
||||
@ -403,14 +438,22 @@ public class DiscoveryActivity extends GBActivity implements AdapterView.OnItemC
|
||||
// New BTLE Discovery use startScan (List<ScanFilter> filters,
|
||||
// ScanSettings settings,
|
||||
// ScanCallback callback)
|
||||
// Its added on API21
|
||||
// It's added on API21
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void startNEWBTLEDiscovery() {
|
||||
// Only use new APi when user use Lollipop+ device
|
||||
// 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(null, getScanSettings(), getScanCallback());
|
||||
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)
|
||||
|
@ -24,7 +24,10 @@ 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;
|
||||
@ -69,6 +72,8 @@ public class ExternalPebbleJSActivity extends GBActivity {
|
||||
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");
|
||||
@ -272,9 +277,28 @@ public class ExternalPebbleJSActivity extends GBActivity {
|
||||
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 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/
|
||||
//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();
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
|
||||
|
||||
protected abstract void refreshList();
|
||||
protected abstract List<GBDeviceApp> getSystemAppsInCategory();
|
||||
|
||||
protected abstract String getSortFilename();
|
||||
|
||||
@ -62,6 +62,23 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
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);
|
||||
@ -103,29 +120,6 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
private GBDeviceAppAdapter mGBDeviceAppAdapter;
|
||||
protected GBDevice mGBDevice = null;
|
||||
|
||||
protected 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.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(UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), "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 && !"aplite".equals(PebbleUtils.getPlatformName(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));
|
||||
}
|
||||
|
||||
return systemApps;
|
||||
}
|
||||
|
||||
protected List<GBDeviceApp> getSystemWatchfaces() {
|
||||
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));
|
||||
return systemWatchfaces;
|
||||
}
|
||||
|
||||
protected List<GBDeviceApp> getCachedApps(List<UUID> uuids) {
|
||||
List<GBDeviceApp> cachedAppList = new ArrayList<>();
|
||||
File cachePath;
|
||||
@ -188,10 +182,23 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
}
|
||||
*/
|
||||
if (mGBDevice != null && !"aplite".equals(PebbleUtils.getPlatformName(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 (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 (uuids == null) {
|
||||
@ -281,6 +288,10 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
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 (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) {
|
||||
menu.removeItem(R.id.appmanager_app_delete);
|
||||
}
|
||||
@ -310,7 +321,6 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
|
||||
public boolean onContextItemSelected(MenuItem item, GBDeviceApp selectedApp) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.appmanager_health_deactivate:
|
||||
case R.id.appmanager_app_delete_cache:
|
||||
String baseName;
|
||||
try {
|
||||
@ -354,6 +364,13 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
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_health_deactivate:
|
||||
case R.id.appmanager_hrm_deactivate:
|
||||
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
|
||||
return true;
|
||||
case R.id.appmanager_app_configure:
|
||||
GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
|
||||
public class AppManagerFragmentCache extends AbstractAppManagerFragment {
|
||||
@ -14,6 +16,11 @@ public class AppManagerFragmentCache extends AbstractAppManagerFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<GBDeviceApp> getSystemAppsInCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSortFilename() {
|
||||
return "pbwcacheorder.txt";
|
||||
|
@ -1,23 +1,36 @@
|
||||
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 void refreshList() {
|
||||
appList.clear();
|
||||
ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename());
|
||||
if (uuids.isEmpty()) {
|
||||
appList.addAll(getSystemApps());
|
||||
for (GBDeviceApp gbDeviceApp : appList) {
|
||||
uuids.add(gbDeviceApp.getUUID());
|
||||
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(UUID.fromString("b2cae818-10f8-46df-ad2b-98ad2254a3c1"), "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));
|
||||
}
|
||||
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids);
|
||||
} else {
|
||||
appList.addAll(getCachedApps(uuids));
|
||||
}
|
||||
|
||||
return systemApps;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,23 +1,19 @@
|
||||
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 void refreshList() {
|
||||
appList.clear();
|
||||
ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename());
|
||||
if (uuids.isEmpty()) {
|
||||
appList.addAll(getSystemWatchfaces());
|
||||
for (GBDeviceApp gbDeviceApp : appList) {
|
||||
uuids.add(gbDeviceApp.getUUID());
|
||||
}
|
||||
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids);
|
||||
} else {
|
||||
appList.addAll(getCachedApps(uuids));
|
||||
}
|
||||
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
|
||||
|
@ -322,10 +322,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
private int getTSLast24Hours(int tsTo) {
|
||||
return (tsTo) - (24 * 60 * 60); // -24 hours
|
||||
}
|
||||
|
||||
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);
|
||||
@ -337,18 +333,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
return provider.getSleepSamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
protected List<? extends 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<? extends ActivitySample> provider = getProvider(db, device);
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
protected void configureChartDefaults(Chart<?> chart) {
|
||||
chart.getXAxis().setValueFormatter(new TimestampValueFormatter());
|
||||
chart.getDescription().setText("");
|
||||
@ -362,7 +346,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
// enable touch gestures
|
||||
chart.setTouchEnabled(true);
|
||||
|
||||
chart.getXAxis().setGranularity(60*5);
|
||||
// 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);
|
||||
}
|
||||
@ -709,7 +696,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device, getTSStart(), getTSEnd());
|
||||
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);
|
||||
@ -720,6 +710,33 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
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;
|
||||
}
|
||||
|
||||
private int getTSEnd() {
|
||||
return toTimestamp(getEndDate());
|
||||
}
|
||||
@ -770,11 +787,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
String dateString = annotationDateFormat.format(date);
|
||||
return dateString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecimalDigits() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
|
||||
@ -792,11 +804,6 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
}
|
||||
return xLabels.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecimalDigits() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,5 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.Date;
|
||||
|
@ -32,9 +32,4 @@ public class TimestampValueFormatter implements IAxisValueFormatter {
|
||||
String dateString = dateFormat.format(date);
|
||||
return dateString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecimalDigits() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -192,6 +192,7 @@ public class WeekStepsChartFragment extends AbstractChartFragment {
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
y.setDrawZeroLine(true);
|
||||
y.setSpaceBottom(0);
|
||||
y.setAxisMinimum(0);
|
||||
|
||||
y.setEnabled(true);
|
||||
|
||||
|
@ -344,6 +344,9 @@ public class DBHelper {
|
||||
if (!Objects.equals(attr.getFirmwareVersion2(), gbDevice.getFirmwareVersion2())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(attr.getVolatileIdentifier(), gbDevice.getVolatileAddress())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -454,6 +457,7 @@ public class DBHelper {
|
||||
attributes.setValidFromUTC(now.getTime());
|
||||
attributes.setFirmwareVersion1(gbDevice.getFirmwareVersion());
|
||||
attributes.setFirmwareVersion2(gbDevice.getFirmwareVersion2());
|
||||
attributes.setVolatileIdentifier(gbDevice.getVolatileAddress());
|
||||
DeviceAttributesDao attributesDao = session.getDeviceAttributesDao();
|
||||
attributesDao.insert(attributes);
|
||||
|
||||
@ -685,4 +689,13 @@ public class DBHelper {
|
||||
}
|
||||
return cursor.getInt(columnIndex);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
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) {
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
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) {
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ public class SchemaMigration {
|
||||
|
||||
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
|
||||
try {
|
||||
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".schema." + classNamePrefix + version);
|
||||
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + "." + classNamePrefix + version);
|
||||
return (DBUpdateScript) updateClass.newInstance();
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
|
@ -12,12 +12,14 @@ public class GBDeviceEventAppManagement extends GBDeviceEvent {
|
||||
UNKNOWN,
|
||||
INSTALL,
|
||||
DELETE,
|
||||
START,
|
||||
STOP,
|
||||
}
|
||||
|
||||
public enum Event {
|
||||
UNKNOWN,
|
||||
SUCCESS,
|
||||
ACKNOLEDGE,
|
||||
ACKNOWLEDGE,
|
||||
FAILURE,
|
||||
REQUEST,
|
||||
}
|
||||
|
@ -2,11 +2,15 @@ package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
import android.bluetooth.BluetoothClass;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
@ -21,12 +25,22 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class);
|
||||
|
||||
@Override
|
||||
public final boolean supports(GBDeviceCandidate candidate) {
|
||||
return getSupportedType(candidate).isSupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDevice device) {
|
||||
return getDeviceType().equals(device.getType());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice createDevice(GBDeviceCandidate candidate) {
|
||||
return new GBDevice(candidate.getDevice().getAddress(), candidate.getName(), getDeviceType());
|
||||
|
@ -74,6 +74,26 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
getSampleDao().insertOrReplaceInTx(activitySamples);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public T getLatestActivitySample() {
|
||||
QueryBuilder<T> qb = getSampleDao().queryBuilder();
|
||||
Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
|
||||
if (dbDevice == null) {
|
||||
// no device, no sample
|
||||
return null;
|
||||
}
|
||||
Property deviceProperty = getDeviceIdentifierSampleProperty();
|
||||
qb.where(deviceProperty.eq(dbDevice.getId())).orderDesc(getTimestampSampleProperty()).limit(1);
|
||||
List<T> samples = qb.build().list();
|
||||
if (samples.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
T sample = samples.get(0);
|
||||
sample.setProvider(this);
|
||||
return sample;
|
||||
}
|
||||
|
||||
protected List<T> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
|
||||
if (getRawKindSampleProperty() == null && activityType != ActivityKind.TYPE_ALL) {
|
||||
// if we do not have a raw kind property we cannot query anything else then TYPE_ALL
|
||||
|
@ -1,8 +1,14 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@ -24,7 +30,18 @@ public interface DeviceCoordinator {
|
||||
String EXTRA_DEVICE_MAC_ADDRESS = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_MAC_ADDRESS";
|
||||
|
||||
/**
|
||||
* Checks whether this candidate handles the given candidate.
|
||||
* Checks whether this coordinator handles the given candidate.
|
||||
* Returns the supported device type for the given candidate or
|
||||
* DeviceType.UNKNOWN
|
||||
*
|
||||
* @param candidate
|
||||
* @return the supported device type for the given candidate.
|
||||
*/
|
||||
@NonNull
|
||||
DeviceType getSupportedType(GBDeviceCandidate candidate);
|
||||
|
||||
/**
|
||||
* Checks whether this coordinator handles the given candidate.
|
||||
*
|
||||
* @param candidate
|
||||
* @return true if this coordinator handles the given candidate.
|
||||
@ -39,6 +56,15 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
boolean supports(GBDevice device);
|
||||
|
||||
/**
|
||||
* Returns a list of scan filters that shall be used to discover devices supported
|
||||
* by this coordinator.
|
||||
* @return the list of scan filters, may be empty
|
||||
*/
|
||||
@NonNull
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
Collection<? extends ScanFilter> createBLEScanFilters();
|
||||
|
||||
GBDevice createDevice(GBDeviceCandidate candidate);
|
||||
|
||||
/**
|
||||
@ -122,7 +148,7 @@ public interface DeviceCoordinator {
|
||||
boolean supportsScreenshots();
|
||||
|
||||
/**
|
||||
* Returns true if this device/coordinator supports settig alarms.
|
||||
* Returns true if this device/coordinator supports setting alarms.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@ -154,5 +180,4 @@ public interface DeviceCoordinator {
|
||||
* @return
|
||||
*/
|
||||
Class<? extends Activity> getAppsManagementActivity();
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
@ -14,7 +14,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
|
||||
/**
|
||||
* Specifies all events that GadgetBridge intends to send to the gadget device.
|
||||
* Specifies all events that Gadgetbridge intends to send to the gadget device.
|
||||
* Implementations can decide to ignore events that they do not support.
|
||||
* Implementations need to send/encode event to the connected device.
|
||||
*/
|
||||
@ -67,5 +67,12 @@ public interface EventHandler {
|
||||
|
||||
void onDeleteCalendarEvent(byte type, long id);
|
||||
|
||||
/**
|
||||
* Sets the given option in the device, typically with values from the preferences.
|
||||
* The config name is device specific.
|
||||
* @param config the device specific option to set on the device
|
||||
*/
|
||||
void onSendConfiguration(String config);
|
||||
|
||||
void onTestNewFunction();
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -22,6 +23,7 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
int PROVIDER_PEBBLE_GADGETBRIDGE = 2; // removed
|
||||
int PROVIDER_PEBBLE_MISFIT = 3;
|
||||
int PROVIDER_PEBBLE_HEALTH = 4;
|
||||
int PROVIDER_MIBAND2 = 5;
|
||||
|
||||
int PROVIDER_UNKNOWN = 100;
|
||||
// TODO: can also be removed
|
||||
@ -87,4 +89,11 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
* @return the newly created "empty" sample
|
||||
*/
|
||||
T createActivitySample();
|
||||
|
||||
/**
|
||||
* Returns the activity sample with the highest timestamp. or null if none
|
||||
* @return the latest sample or null
|
||||
*/
|
||||
@Nullable
|
||||
T getLatestActivitySample();
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -64,6 +65,12 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractActivitySample getLatestActivitySample() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() {
|
||||
return PROVIDER_UNKNOWN;
|
||||
@ -75,8 +82,8 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
return false;
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,110 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.liveview;
|
||||
//Changed by Renze: Fixed brightness constants
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Message constants reverse-engineered by Andrew de Quincey (<a
|
||||
* href="http://adq.livejournal.com">http://adq.livejournal.com</a>).
|
||||
*
|
||||
* @author Robert <xperimental@solidproject.de>
|
||||
*/
|
||||
public final class LiveviewConstants {
|
||||
|
||||
public static Charset ENCODING = StandardCharsets.ISO_8859_1;
|
||||
public static ByteOrder BYTE_ORDER = ByteOrder.BIG_ENDIAN;
|
||||
|
||||
public static final byte CLOCK_24H = 0;
|
||||
public static final byte CLOCK_12H = 1;
|
||||
|
||||
public static final byte MSG_GETCAPS = 1;
|
||||
public static final byte MSG_GETCAPS_RESP = 2;
|
||||
|
||||
public static final byte MSG_DISPLAYTEXT = 3;
|
||||
public static final byte MSG_DISPLAYTEXT_ACK = 4;
|
||||
|
||||
public static final byte MSG_DISPLAYPANEL = 5;
|
||||
public static final byte MSG_DISPLAYPANEL_ACK = 6;
|
||||
|
||||
public static final byte MSG_DEVICESTATUS = 7;
|
||||
public static final byte MSG_DEVICESTATUS_ACK = 8;
|
||||
|
||||
public static final byte MSG_DISPLAYBITMAP = 19;
|
||||
public static final byte MSG_DISPLAYBITMAP_ACK = 20;
|
||||
|
||||
public static final byte MSG_CLEARDISPLAY = 21;
|
||||
public static final byte MSG_CLEARDISPLAY_ACK = 22;
|
||||
|
||||
public static final byte MSG_SETMENUSIZE = 23;
|
||||
public static final byte MSG_SETMENUSIZE_ACK = 24;
|
||||
|
||||
public static final byte MSG_GETMENUITEM = 25;
|
||||
public static final byte MSG_GETMENUITEM_RESP = 26;
|
||||
|
||||
public static final byte MSG_GETALERT = 27;
|
||||
public static final byte MSG_GETALERT_RESP = 28;
|
||||
|
||||
public static final byte MSG_NAVIGATION = 29;
|
||||
public static final byte MSG_NAVIGATION_RESP = 30;
|
||||
|
||||
public static final byte MSG_SETSTATUSBAR = 33;
|
||||
public static final byte MSG_SETSTATUSBAR_ACK = 34;
|
||||
|
||||
public static final byte MSG_GETMENUITEMS = 35;
|
||||
|
||||
public static final byte MSG_SETMENUSETTINGS = 36;
|
||||
public static final byte MSG_SETMENUSETTINGS_ACK = 37;
|
||||
|
||||
public static final byte MSG_GETTIME = 38;
|
||||
public static final byte MSG_GETTIME_RESP = 39;
|
||||
|
||||
public static final byte MSG_SETLED = 40;
|
||||
public static final byte MSG_SETLED_ACK = 41;
|
||||
|
||||
public static final byte MSG_SETVIBRATE = 42;
|
||||
public static final byte MSG_SETVIBRATE_ACK = 43;
|
||||
|
||||
public static final byte MSG_ACK = 44;
|
||||
|
||||
public static final byte MSG_SETSCREENMODE = 64;
|
||||
public static final byte MSG_SETSCREENMODE_ACK = 65;
|
||||
|
||||
public static final byte MSG_GETSCREENMODE = 66;
|
||||
public static final byte MSG_GETSCREENMODE_RESP = 67;
|
||||
|
||||
public static final int DEVICESTATUS_OFF = 0;
|
||||
public static final int DEVICESTATUS_ON = 1;
|
||||
public static final int DEVICESTATUS_MENU = 2;
|
||||
|
||||
public static final byte RESULT_OK = 0;
|
||||
public static final byte RESULT_ERROR = 1;
|
||||
public static final byte RESULT_OOM = 2;
|
||||
public static final byte RESULT_EXIT = 3;
|
||||
public static final byte RESULT_CANCEL = 4;
|
||||
|
||||
public static final int NAVACTION_PRESS = 0;
|
||||
public static final int NAVACTION_LONGPRESS = 1;
|
||||
public static final int NAVACTION_DOUBLEPRESS = 2;
|
||||
|
||||
public static final int NAVTYPE_UP = 0;
|
||||
public static final int NAVTYPE_DOWN = 1;
|
||||
public static final int NAVTYPE_LEFT = 2;
|
||||
public static final int NAVTYPE_RIGHT = 3;
|
||||
public static final int NAVTYPE_SELECT = 4;
|
||||
public static final int NAVTYPE_MENUSELECT = 5;
|
||||
|
||||
public static final int ALERTACTION_CURRENT = 0;
|
||||
public static final int ALERTACTION_FIRST = 1;
|
||||
public static final int ALERTACTION_LAST = 2;
|
||||
public static final int ALERTACTION_NEXT = 3;
|
||||
public static final int ALERTACTION_PREV = 4;
|
||||
|
||||
public static final int BRIGHTNESS_OFF = 49;
|
||||
public static final int BRIGHTNESS_DIM = 50;
|
||||
public static final int BRIGHTNESS_MAX = 51;
|
||||
|
||||
public static final String CLIENT_SOFTWARE_VERSION = "0.0.3";
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.liveview;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class LiveviewCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
if (name != null && name.startsWith("LiveView")) {
|
||||
return DeviceType.LIVEVIEW;
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.LIVEVIEW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPrimaryActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmConfiguration() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTapString() {
|
||||
//TODO: changeme
|
||||
return R.string.tap_connected_device_for_activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Sony Ericsson";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
// nothing to delete, yet
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
/**
|
||||
* Also see Mi1SFirmwareInfo.
|
||||
*/
|
||||
public abstract class AbstractMiBandFWHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWHelper.class);
|
||||
|
||||
@NonNull
|
||||
private final byte[] fw;
|
||||
|
||||
public AbstractMiBandFWHelper(Uri uri, Context context) throws IOException {
|
||||
String pebblePattern = ".*\\.(pbw|pbz|pbl)";
|
||||
if (uri.getPath().matches(pebblePattern)) {
|
||||
throw new IOException("Firmware has a filename that looks like a Pebble app/firmware.");
|
||||
}
|
||||
|
||||
try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) {
|
||||
this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB
|
||||
determineFirmwareInfo(fw);
|
||||
} catch (IOException ex) {
|
||||
throw ex; // pass through
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IOException("This doesn't seem to be a Mi Band firmware: " + ex.getLocalizedMessage(), ex);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error reading firmware file: " + uri.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract int getFirmwareVersion();
|
||||
|
||||
public abstract int getFirmware2Version();
|
||||
|
||||
public static String formatFirmwareVersion(int version) {
|
||||
if (version == -1)
|
||||
return GBApplication.getContext().getString(R.string._unknown_);
|
||||
|
||||
return String.format("%d.%d.%d.%d",
|
||||
version >> 24 & 255,
|
||||
version >> 16 & 255,
|
||||
version >> 8 & 255,
|
||||
version & 255);
|
||||
}
|
||||
|
||||
public String getHumanFirmwareVersion() {
|
||||
return format(getFirmwareVersion());
|
||||
}
|
||||
|
||||
public abstract String getHumanFirmwareVersion2();
|
||||
|
||||
public String format(int version) {
|
||||
return formatFirmwareVersion(version);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public byte[] getFw() {
|
||||
return fw;
|
||||
}
|
||||
|
||||
public boolean isFirmwareWhitelisted() {
|
||||
for (int wlf : getWhitelistedFirmwareVersions()) {
|
||||
if (wlf == getFirmwareVersion()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract int[] getWhitelistedFirmwareVersions();
|
||||
|
||||
public abstract boolean isFirmwareGenerallyCompatibleWith(GBDevice device);
|
||||
|
||||
public abstract boolean isSingleFirmware();
|
||||
|
||||
/**
|
||||
* @param wholeFirmwareBytes
|
||||
* @return
|
||||
* @throws IllegalArgumentException when the data is not recognized as firmware data
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract void determineFirmwareInfo(byte[] wholeFirmwareBytes);
|
||||
|
||||
public abstract void checkValid() throws IllegalArgumentException;
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
|
||||
public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWInstallHandler.class);
|
||||
|
||||
private final Context mContext;
|
||||
private AbstractMiBandFWHelper helper;
|
||||
private String errorMessage;
|
||||
|
||||
public AbstractMiBandFWInstallHandler(Uri uri, Context context) {
|
||||
mContext = context;
|
||||
|
||||
try {
|
||||
helper = createHelper(uri, context);
|
||||
} catch (IOException e) {
|
||||
errorMessage = e.getMessage();
|
||||
LOG.warn(errorMessage, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException;
|
||||
|
||||
|
||||
@Override
|
||||
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
|
||||
if (device.isBusy()) {
|
||||
installActivity.setInfoText(device.getBusyTask());
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSupportedDeviceType(device) || !device.isInitialized()) {
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
helper.checkValid();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
installActivity.setInfoText(ex.getLocalizedMessage());
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion()));
|
||||
fwItem.setIcon(R.drawable.ic_device_miband);
|
||||
|
||||
if (!helper.isFirmwareGenerallyCompatibleWith(device)) {
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (helper.isSingleFirmware()) {
|
||||
builder.append(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion()));
|
||||
} else {
|
||||
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
|
||||
}
|
||||
|
||||
|
||||
if (helper.isFirmwareWhitelisted()) {
|
||||
builder.append(" ").append(mContext.getString(R.string.miband_firmware_known));
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version));
|
||||
// TODO: set a CHECK (OKAY) button
|
||||
} else {
|
||||
builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n")
|
||||
.append(mContext.getString(R.string.miband_firmware_suggest_whitelist, helper.getFirmwareVersion()));
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
|
||||
// TODO: set a UNKNOWN (question mark) button
|
||||
}
|
||||
installActivity.setInfoText(builder.toString());
|
||||
installActivity.setInstallItem(fwItem);
|
||||
installActivity.setInstallEnabled(true);
|
||||
}
|
||||
|
||||
protected abstract boolean isSupportedDeviceType(GBDevice device);
|
||||
|
||||
@Override
|
||||
public void onStartInstall(GBDevice device) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return helper != null;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
/**
|
||||
* Base class for Mi1 and Mi2 sample providers. At the moment they both share the
|
||||
* same activity sample class.
|
||||
*/
|
||||
public abstract class AbstractMiBandSampleProvider extends AbstractSampleProvider<MiBandActivitySample> {
|
||||
|
||||
// maybe this should be configurable 256 seems way off, though.
|
||||
private final float movementDivisor = 180.0f; //256.0f;
|
||||
|
||||
public AbstractMiBandSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity / movementDivisor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<MiBandActivitySample, ?> getSampleDao() {
|
||||
return getSession().getMiBandActivitySampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return MiBandActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return MiBandActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return MiBandActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MiBandActivitySample createActivitySample() {
|
||||
return new MiBandActivitySample();
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
public enum DateTimeDisplay {
|
||||
TIME,
|
||||
DATE_TIME
|
||||
}
|
@ -1,16 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2FWInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class MiBand2Coordinator extends MiBandCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBand2Coordinator.class);
|
||||
@ -20,25 +35,41 @@ public class MiBand2Coordinator extends MiBandCoordinator {
|
||||
return DeviceType.MIBAND2;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
// and a heuristic
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
ParcelUuid mi2Service = new ParcelUuid(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
|
||||
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi2Service).build();
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
if (candidate.supportsService(MiBand2Service.UUID_SERVICE_MIBAND2_SERVICE)) {
|
||||
return DeviceType.MIBAND2;
|
||||
}
|
||||
|
||||
// and a heuristic for now
|
||||
try {
|
||||
BluetoothDevice device = candidate.getDevice();
|
||||
if (isHealthWearable(device)) {
|
||||
String name = device.getName();
|
||||
return name != null && name.equalsIgnoreCase(MiBandConst.MI_BAND2_NAME);
|
||||
if (name != null && name.equalsIgnoreCase(MiBandConst.MI_BAND2_NAME)) {
|
||||
return DeviceType.MIBAND2;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
return DeviceType.UNKNOWN;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false; // not yet
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -48,11 +79,31 @@ public class MiBand2Coordinator extends MiBandCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return false; // not yet
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new MiBand2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
public static DateTimeDisplay getDateDisplay(Context context) throws IllegalArgumentException {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
String dateFormatTime = context.getString(R.string.p_dateformat_time);
|
||||
if (dateFormatTime.equals(prefs.getString(MiBandConst.PREF_MI2_DATEFORMAT, dateFormatTime))) {
|
||||
return DateTimeDisplay.TIME;
|
||||
}
|
||||
return DateTimeDisplay.DATE_TIME;
|
||||
}
|
||||
|
||||
public static boolean getActivateDisplayOnLiftWrist() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null; // not supported at the moment
|
||||
MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context);
|
||||
return handler.isValid() ? handler : null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,138 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class MiBand2SampleProvider extends AbstractMiBandSampleProvider {
|
||||
// these come from Mi1
|
||||
// public static final int TYPE_LIGHT_SLEEP = 5;
|
||||
// public static final int TYPE_ACTIVITY = -1;
|
||||
// public static final int TYPE_UNKNOWN = -1;
|
||||
// public static final int TYPE_NONWEAR = 3;
|
||||
// public static final int TYPE_CHARGING = 6;
|
||||
|
||||
|
||||
// observed the following values so far:
|
||||
// 00 01 02 09 0a 0b 0c 10 11
|
||||
|
||||
// 0 = same activity kind as before
|
||||
// 1 = light activity walking?
|
||||
// 3 = definitely non-wear
|
||||
// 9 = probably light sleep, definitely some kind of sleep
|
||||
// 10 = ignore, except for hr (if valid)
|
||||
// 11 = probably deep sleep
|
||||
// 12 = definitely wake up
|
||||
// 17 = definitely not sleep related
|
||||
|
||||
public static final int TYPE_UNSET = -1;
|
||||
public static final int TYPE_NO_CHANGE = 0;
|
||||
public static final int TYPE_ACTIVITY = 1;
|
||||
public static final int TYPE_NONWEAR = 3;
|
||||
public static final int TYPE_CHARGING = 6;
|
||||
public static final int TYPE_LIGHT_SLEEP = 9;
|
||||
public static final int TYPE_DEEP_SLEEP = 11;
|
||||
public static final int TYPE_WAKE_UP = 12;
|
||||
// appears to be a measurement problem resulting in type = 10 and intensity = 20, at least with fw 1.0.0.39
|
||||
public static final int TYPE_IGNORE = 10;
|
||||
|
||||
public MiBand2SampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() {
|
||||
return SampleProvider.PROVIDER_MIBAND2;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<MiBandActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) {
|
||||
List<MiBandActivitySample> samples = super.getGBActivitySamples(timestamp_from, timestamp_to, activityType);
|
||||
postprocess(samples);
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Temporary" runtime post processing of activity kinds.
|
||||
* @param samples
|
||||
*/
|
||||
private void postprocess(List<MiBandActivitySample> samples) {
|
||||
if (samples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int lastValidKind = determinePreviousValidActivityType(samples.get(0));
|
||||
for (MiBandActivitySample sample : samples) {
|
||||
int rawKind = sample.getRawKind();
|
||||
switch (rawKind) {
|
||||
case TYPE_IGNORE:
|
||||
case TYPE_NO_CHANGE:
|
||||
if (lastValidKind != TYPE_UNSET) {
|
||||
sample.setRawKind(lastValidKind);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
lastValidKind = rawKind;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int determinePreviousValidActivityType(MiBandActivitySample sample) {
|
||||
QueryBuilder<MiBandActivitySample> qb = getSampleDao().queryBuilder();
|
||||
qb.where(MiBandActivitySampleDao.Properties.DeviceId.eq(sample.getDeviceId()),
|
||||
MiBandActivitySampleDao.Properties.UserId.eq(sample.getUserId()),
|
||||
MiBandActivitySampleDao.Properties.Timestamp.lt(sample.getTimestamp()),
|
||||
MiBandActivitySampleDao.Properties.RawKind.notIn(TYPE_IGNORE, TYPE_NO_CHANGE));
|
||||
qb.limit(1);
|
||||
List<MiBandActivitySample> result = qb.build().list();
|
||||
if (result.size() > 0) {
|
||||
return result.get(0).getRawKind();
|
||||
}
|
||||
return TYPE_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(int rawType) {
|
||||
switch (rawType) {
|
||||
case TYPE_DEEP_SLEEP:
|
||||
return ActivityKind.TYPE_DEEP_SLEEP;
|
||||
case TYPE_LIGHT_SLEEP:
|
||||
return ActivityKind.TYPE_LIGHT_SLEEP;
|
||||
case TYPE_ACTIVITY:
|
||||
return ActivityKind.TYPE_ACTIVITY;
|
||||
case TYPE_NONWEAR:
|
||||
return ActivityKind.TYPE_NOT_WORN;
|
||||
case TYPE_CHARGING:
|
||||
return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption
|
||||
case TYPE_IGNORE:
|
||||
default:
|
||||
case TYPE_UNSET: // fall through
|
||||
return ActivityKind.TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(int activityKind) {
|
||||
switch (activityKind) {
|
||||
case ActivityKind.TYPE_ACTIVITY:
|
||||
return TYPE_ACTIVITY;
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
return TYPE_DEEP_SLEEP;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
return TYPE_LIGHT_SLEEP;
|
||||
case ActivityKind.TYPE_NOT_WORN:
|
||||
return TYPE_NONWEAR;
|
||||
case ActivityKind.TYPE_UNKNOWN: // fall through
|
||||
default:
|
||||
return TYPE_UNSET;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,14 +12,21 @@ public class MiBand2Service {
|
||||
public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0"));
|
||||
public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1"));
|
||||
public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D"));
|
||||
public static final UUID UUID_SERVICE_WEIGHT_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_SERVICE_FIRMWARE_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700");
|
||||
|
||||
public static final UUID UUID_CHARACTERISTIC_FIRMWARE = UUID.fromString("00001531-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString("00001532-0000-3512-2118-0009af100700");
|
||||
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC0 = UUID.fromString("00000000-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC1 = UUID.fromString("00000001-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC2 = UUID.fromString("00000002-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC3 = UUID.fromString("00000003-0000-3512-2118-0009af100700"); // Alarm related
|
||||
/**
|
||||
* Alarms, Display and other configuration.
|
||||
*/
|
||||
public static final UUID UUID_CHARACTERISTIC_3_CONFIGURATION = UUID.fromString("00000003-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC4 = UUID.fromString("00000004-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC5 = UUID.fromString("00000005-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC6 = UUID.fromString("00000006-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_5_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_6_BATTERY_INFO = UUID.fromString("00000006-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-0000-3512-2118-0009af100700");
|
||||
// service uuid fee1
|
||||
@ -35,206 +42,6 @@ public class MiBand2Service {
|
||||
// set 12 hour time mode
|
||||
|
||||
|
||||
// public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_DEVICE_NAME = UUID.fromString(String.format(BASE_UUID, "FF02"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_NOTIFICATION = UUID.fromString(String.format(BASE_UUID, "FF03"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_USER_INFO = UUID.fromString(String.format(BASE_UUID, "FF04"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "FF05"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_REALTIME_STEPS = UUID.fromString(String.format(BASE_UUID, "FF06"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString(String.format(BASE_UUID, "FF07"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString(String.format(BASE_UUID, "FF08"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_LE_PARAMS = UUID.fromString(String.format(BASE_UUID, "FF09"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_DATE_TIME = UUID.fromString(String.format(BASE_UUID, "FF0A"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_STATISTICS = UUID.fromString(String.format(BASE_UUID, "FF0B"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_BATTERY = UUID.fromString(String.format(BASE_UUID, "FF0C"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_TEST = UUID.fromString(String.format(BASE_UUID, "FF0D"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_SENSOR_DATA = UUID.fromString(String.format(BASE_UUID, "FF0E"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F"));
|
||||
//
|
||||
// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "2A39"));
|
||||
// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = UUID.fromString(String.format(BASE_UUID, "2A37"));
|
||||
//
|
||||
//
|
||||
//
|
||||
// /* FURTHER UUIDS that were mixed with the other params below. The base UUID for these is unknown */
|
||||
//
|
||||
// public static final byte ALIAS_LEN = 0xa;
|
||||
//
|
||||
// /*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */
|
||||
//
|
||||
// public static final byte NOTIFY_NORMAL = 0x0;
|
||||
//
|
||||
// public static final byte NOTIFY_FIRMWARE_UPDATE_FAILED = 0x1;
|
||||
//
|
||||
// public static final byte NOTIFY_FIRMWARE_UPDATE_SUCCESS = 0x2;
|
||||
//
|
||||
// public static final byte NOTIFY_CONN_PARAM_UPDATE_FAILED = 0x3;
|
||||
//
|
||||
// public static final byte NOTIFY_CONN_PARAM_UPDATE_SUCCESS = 0x4;
|
||||
//
|
||||
// public static final byte NOTIFY_AUTHENTICATION_SUCCESS = 0x5;
|
||||
//
|
||||
// public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6;
|
||||
//
|
||||
// public static final byte NOTIFY_FITNESS_GOAL_ACHIEVED = 0x7;
|
||||
//
|
||||
// public static final byte NOTIFY_SET_LATENCY_SUCCESS = 0x8;
|
||||
//
|
||||
// public static final byte NOTIFY_RESET_AUTHENTICATION_FAILED = 0x9;
|
||||
//
|
||||
// public static final byte NOTIFY_RESET_AUTHENTICATION_SUCCESS = 0xa;
|
||||
//
|
||||
// public static final byte NOTIFY_FW_CHECK_FAILED = 0xb;
|
||||
//
|
||||
// public static final byte NOTIFY_FW_CHECK_SUCCESS = 0xc;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_NOTIFY = 0xd;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_CALL = 0xe;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_DISCONNECT = 0xf;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_SMART_ALARM = 0x10;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_ALARM = 0x11;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_GOAL = 0x12;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_AUTH = 0x13;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_SHUTDOWN = 0x14;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_AUTH_SUCCESS = 0x15;
|
||||
//
|
||||
// public static final byte NOTIFY_STATUS_MOTOR_TEST = 0x16;
|
||||
//
|
||||
// // 0x18 is returned when we cancel data sync, perhaps is an ack for this message
|
||||
//
|
||||
// public static final byte NOTIFY_UNKNOWN = -0x1;
|
||||
//
|
||||
// public static final int NOTIFY_PAIR_CANCEL = 0xef;
|
||||
//
|
||||
// public static final int NOTIFY_DEVICE_MALFUNCTION = 0xff;
|
||||
//
|
||||
//
|
||||
// /* MESSAGES: unknown */
|
||||
//
|
||||
// public static final byte MSG_CONNECTED = 0x0;
|
||||
//
|
||||
// public static final byte MSG_DISCONNECTED = 0x1;
|
||||
//
|
||||
// public static final byte MSG_CONNECTION_FAILED = 0x2;
|
||||
//
|
||||
// public static final byte MSG_INITIALIZATION_FAILED = 0x3;
|
||||
//
|
||||
// public static final byte MSG_INITIALIZATION_SUCCESS = 0x4;
|
||||
//
|
||||
// public static final byte MSG_STEPS_CHANGED = 0x5;
|
||||
//
|
||||
// public static final byte MSG_DEVICE_STATUS_CHANGED = 0x6;
|
||||
//
|
||||
// public static final byte MSG_BATTERY_STATUS_CHANGED = 0x7;
|
||||
//
|
||||
// /* COMMANDS: usually sent to UUID_CHARACTERISTIC_CONTROL_POINT characteristic */
|
||||
//
|
||||
// public static final byte COMMAND_SET_TIMER = 0x4;
|
||||
//
|
||||
// public static final byte COMMAND_SET_FITNESS_GOAL = 0x5;
|
||||
//
|
||||
// public static final byte COMMAND_FETCH_DATA = 0x6;
|
||||
//
|
||||
// public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7;
|
||||
//
|
||||
// public static final byte COMMAND_SEND_NOTIFICATION = 0x8;
|
||||
//
|
||||
// public static final byte COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xa;
|
||||
//
|
||||
// public static final byte COMMAND_SYNC = 0xb;
|
||||
//
|
||||
// public static final byte COMMAND_REBOOT = 0xc;
|
||||
//
|
||||
// public static final byte COMMAND_SET_WEAR_LOCATION = 0xf;
|
||||
//
|
||||
// public static final byte COMMAND_STOP_SYNC_DATA = 0x11;
|
||||
//
|
||||
// public static final byte COMMAND_STOP_MOTOR_VIBRATE = 0x13;
|
||||
//
|
||||
// public static final byte COMMAND_SET_REALTIME_STEPS_NOTIFICATION = 0x3;
|
||||
//
|
||||
// public static final byte COMMAND_SET_REALTIME_STEP = 0x10;
|
||||
//
|
||||
// // Test HR
|
||||
// public static final byte COMMAND_SET_HR_SLEEP = 0x0;
|
||||
// public static final byte COMMAND_SET__HR_CONTINUOUS = 0x1;
|
||||
// public static final byte COMMAND_SET_HR_MANUAL = 0x2;
|
||||
//
|
||||
//
|
||||
// /* FURTHER COMMANDS: unchecked therefore left commented
|
||||
//
|
||||
//
|
||||
// public static final byte COMMAND_FACTORY_RESET = 0x9t;
|
||||
//
|
||||
// public static final int COMMAND_SET_COLOR_THEME = et;
|
||||
//
|
||||
// public static final byte COMMAND_GET_SENSOR_DATA = 0x12t
|
||||
//
|
||||
// */
|
||||
//
|
||||
// /* CONNECTION: unknown
|
||||
//
|
||||
// public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t;
|
||||
//
|
||||
// public static final CONNECTION_LATENCY_LEVEL_MEDIUM = 0x1t;
|
||||
//
|
||||
// public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t;
|
||||
//
|
||||
// */
|
||||
//
|
||||
// /* MODES: probably related to the sample data structure
|
||||
// */
|
||||
//
|
||||
// public static final byte MODE_REGULAR_DATA_LEN_BYTE = 0x0;
|
||||
//
|
||||
// // was MODE_REGULAR_DATA_LEN_MINITE
|
||||
// public static final byte MODE_REGULAR_DATA_LEN_MINUTE = 0x1;
|
||||
//
|
||||
// /* PROFILE: unknown
|
||||
//
|
||||
// public static final PROFILE_STATE_UNKNOWN:I = 0x0
|
||||
//
|
||||
// public static final PROFILE_STATE_INITIALIZATION_SUCCESS:I = 0x1
|
||||
//
|
||||
// public static final PROFILE_STATE_INITIALIZATION_FAILED:I = 0x2
|
||||
//
|
||||
// public static final PROFILE_STATE_AUTHENTICATION_SUCCESS:I = 0x3
|
||||
//
|
||||
// public static final PROFILE_STATE_AUTHENTICATION_FAILED:I = 0x4
|
||||
//
|
||||
// */
|
||||
//
|
||||
// // TEST_*: sent to UUID_CHARACTERISTIC_TEST characteristic
|
||||
//
|
||||
// public static final byte TEST_DISCONNECTED_REMINDER = 0x5;
|
||||
//
|
||||
// public static final byte TEST_NOTIFICATION = 0x3;
|
||||
//
|
||||
// public static final byte TEST_REMOTE_DISCONNECT = 0x1;
|
||||
//
|
||||
// public static final byte TEST_SELFTEST = 0x2;
|
||||
|
||||
private static final Map<UUID, String> MIBAND_DEBUG;
|
||||
|
||||
@ -265,7 +72,7 @@ public class MiBand2Service {
|
||||
*/
|
||||
public static final byte AUTH_RESPONSE = 0x10;
|
||||
/**
|
||||
* Receeived in response to any authentication requests (byte 2 in the byte[] value.
|
||||
* Received in response to any authentication requests (byte 2 in the byte[] value.
|
||||
* 0x01 means success.
|
||||
*/
|
||||
public static final byte AUTH_SUCCESS = 0x01;
|
||||
@ -279,6 +86,49 @@ public class MiBand2Service {
|
||||
*/
|
||||
public static final byte AUTH_BYTE = 0x8;
|
||||
|
||||
// maybe not really activity data, but steps?
|
||||
public static final byte COMMAND_FETCH_ACTIVITY_DATA = 0x02;
|
||||
public static final byte COMMAND_XXXX_ACTIVITY_DATA = 0x03; // maybe delete/drop activity data?
|
||||
|
||||
public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 };
|
||||
public static final byte[] COMMAND_SET_FITNESS_GOAL_END = new byte[] { 0, 0 };
|
||||
|
||||
|
||||
public static byte COMMAND_DATEFORMAT = 0x06;
|
||||
|
||||
public static final byte[] DATEFORMAT_DATE_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x03 };
|
||||
public static final byte[] DATEFORMAT_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x0 };
|
||||
|
||||
public static final byte RESPONSE = 0x10;
|
||||
|
||||
public static final byte SUCCESS = 0x01;
|
||||
public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01;
|
||||
public static final byte COMMAND_ACTIVITY_DATA_XXX_DATE = 0x02; // issued on first connect, followd by COMMAND_XXXX_ACTIVITY_DATA instead of COMMAND_FETCH_ACTIVITY_DATA
|
||||
|
||||
public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE, followed by fw file size in bytes
|
||||
public static final byte COMMAND_FIRMWARE_START_DATA = 0x03; // to UUID_CHARACTERISTIC_FIRMWARE
|
||||
public static final byte COMMAND_FIRMWARE_UPDATE_SYNC = 0x00; // to UUID_CHARACTERISTIC_FIRMWARE
|
||||
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
|
||||
public static final byte COMMAND_FIRMWARE_APPLY_REBOOT = 0x05; // or is it REBOOT? to UUID_CHARACTERISTIC_FIRMWARE
|
||||
|
||||
public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS };
|
||||
public static final byte[] RESPONSE_FIRMWARE_DATA_SUCCESS = new byte[] {RESPONSE, COMMAND_FIRMWARE_START_DATA, SUCCESS };
|
||||
/**
|
||||
* Received in response to any dateformat configuration request (byte 0 in the byte[] value.
|
||||
*/
|
||||
public static final byte[] RESPONSE_DATEFORMAT_SUCCESS = new byte[] { RESPONSE, COMMAND_DATEFORMAT, 0x0a, 0x0, 0x01 };
|
||||
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
|
||||
|
||||
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
|
||||
public static final byte[] WEAR_LOCATION_RIGHT_WRIST = new byte[] { 0x20, 0x00, 0x00, (byte) 0x82};
|
||||
|
||||
public static final byte[] COMMAND_ENABLE_HR_SLEEP_MEASUREMENT = new byte[]{0x15, 0x00, 0x01};
|
||||
public static final byte[] COMMAND_DISABLE_HR_SLEEP_MEASUREMENT = new byte[]{0x15, 0x00, 0x00};
|
||||
|
||||
public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{0x06, 0x05, 0x00, 0x01};
|
||||
public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{0x06, 0x05, 0x00, 0x00};
|
||||
|
||||
|
||||
static {
|
||||
MIBAND_DEBUG = new HashMap<>();
|
||||
MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service");
|
||||
|
@ -17,8 +17,8 @@ public final class MiBandConst {
|
||||
public static final String PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR = "mi_reserve_alarm_calendar";
|
||||
public static final String PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION = "mi_hr_sleep_detection";
|
||||
public static final String PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS = "mi_device_time_offset_hours";
|
||||
|
||||
public static final String PREF_TRY_SMS = "mi_try_sms";
|
||||
public static final String PREF_MI2_DATEFORMAT = "mi2_dateformat";
|
||||
public static final String PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT = "mi2_activate_display_on_lift_wrist";
|
||||
|
||||
|
||||
public static final String ORIGIN_INCOMING_CALL = "incoming_call";
|
||||
|
@ -1,13 +1,21 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
@ -32,28 +40,40 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
|
||||
public MiBandCoordinator() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
ParcelUuid mi1Service = new ParcelUuid(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
|
||||
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi1Service).build();
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String macAddress = candidate.getMacAddress().toUpperCase();
|
||||
if (macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1_1A)
|
||||
|| macAddress.startsWith(MiBandService.MAC_ADDRESS_FILTER_1S)) {
|
||||
return true;
|
||||
return DeviceType.MIBAND;
|
||||
}
|
||||
if (candidate.supportsService(MiBandService.UUID_SERVICE_MIBAND_SERVICE)
|
||||
&& !candidate.supportsService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE)) {
|
||||
return true;
|
||||
return DeviceType.MIBAND;
|
||||
}
|
||||
// and a heuristic
|
||||
try {
|
||||
BluetoothDevice device = candidate.getDevice();
|
||||
if (isHealthWearable(device)) {
|
||||
String name = device.getName();
|
||||
return name != null && name.toUpperCase().startsWith(MiBandConst.MI_GENERAL_NAME_PREFIX.toUpperCase());
|
||||
if (name != null && name.toUpperCase().startsWith(MiBandConst.MI_GENERAL_NAME_PREFIX.toUpperCase())) {
|
||||
return DeviceType.MIBAND;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return false;
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -7,20 +7,15 @@ import android.support.annotation.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
/**
|
||||
* Also see Mi1SFirmwareInfo.
|
||||
*/
|
||||
public class MiBandFWHelper {
|
||||
public class MiBandFWHelper extends AbstractMiBandFWHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class);
|
||||
|
||||
/**
|
||||
@ -29,9 +24,7 @@ public class MiBandFWHelper {
|
||||
* attempting to flash it.
|
||||
*/
|
||||
@NonNull
|
||||
private final AbstractMiFirmwareInfo firmwareInfo;
|
||||
@NonNull
|
||||
private final byte[] fw;
|
||||
private AbstractMiFirmwareInfo firmwareInfo;
|
||||
|
||||
/**
|
||||
* Provides a different notification API which is also used on Mi1A devices.
|
||||
@ -54,77 +47,55 @@ public class MiBandFWHelper {
|
||||
};
|
||||
|
||||
public MiBandFWHelper(Uri uri, Context context) throws IOException {
|
||||
String pebblePattern = ".*\\.(pbw|pbz|pbl)";
|
||||
if (uri.getPath().matches(pebblePattern)) {
|
||||
throw new IOException("Firmware has a filename that looks like a Pebble app/firmware.");
|
||||
}
|
||||
|
||||
try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) {
|
||||
this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB
|
||||
this.firmwareInfo = determineFirmwareInfoFor(fw);
|
||||
} catch (IOException ex) {
|
||||
throw ex; // pass through
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IOException("This doesn't seem to be a Mi Band firmware: " + ex.getLocalizedMessage(), ex);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error reading firmware file: " + uri.toString(), e);
|
||||
}
|
||||
super(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareVersion() {
|
||||
// FIXME: UnsupportedOperationException!
|
||||
return firmwareInfo.getFirst().getFirmwareVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmware2Version() {
|
||||
return firmwareInfo.getFirst().getFirmwareVersion();
|
||||
}
|
||||
|
||||
public static String formatFirmwareVersion(int version) {
|
||||
if (version == -1)
|
||||
return GBApplication.getContext().getString(R.string._unknown_);
|
||||
|
||||
return String.format("%d.%d.%d.%d",
|
||||
version >> 24 & 255,
|
||||
version >> 16 & 255,
|
||||
version >> 8 & 255,
|
||||
version & 255);
|
||||
}
|
||||
|
||||
public String getHumanFirmwareVersion() {
|
||||
return format(getFirmwareVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHumanFirmwareVersion2() {
|
||||
return format(firmwareInfo.getSecond().getFirmwareVersion());
|
||||
}
|
||||
|
||||
public String format(int version) {
|
||||
return formatFirmwareVersion(version);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public byte[] getFw() {
|
||||
return fw;
|
||||
}
|
||||
|
||||
public boolean isFirmwareWhitelisted() {
|
||||
for (int wlf : whitelistedFirmwareVersion) {
|
||||
if (wlf == getFirmwareVersion()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@Override
|
||||
protected int[] getWhitelistedFirmwareVersions() {
|
||||
return whitelistedFirmwareVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
|
||||
return firmwareInfo.isGenerallyCompatibleWith(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleFirmware() {
|
||||
return firmwareInfo.isSingleMiBandFirmware();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param wholeFirmwareBytes
|
||||
* @return
|
||||
* @throws IllegalArgumentException when the data is not recognized as firmware data
|
||||
*/
|
||||
@Override
|
||||
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
|
||||
firmwareInfo = AbstractMiFirmwareInfo.determineFirmwareInfoFor(wholeFirmwareBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
firmwareInfo.checkValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param wholeFirmwareBytes
|
||||
* @return
|
||||
|
@ -8,91 +8,23 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
|
||||
public class MiBandFWInstallHandler implements InstallHandler {
|
||||
public class MiBandFWInstallHandler extends AbstractMiBandFWInstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBandFWInstallHandler.class);
|
||||
|
||||
private final Context mContext;
|
||||
private MiBandFWHelper helper;
|
||||
private String errorMessage;
|
||||
|
||||
public MiBandFWInstallHandler(Uri uri, Context context) {
|
||||
mContext = context;
|
||||
|
||||
try {
|
||||
helper = new MiBandFWHelper(uri, mContext);
|
||||
} catch (IOException e) {
|
||||
errorMessage = e.getMessage();
|
||||
LOG.warn(errorMessage, e);
|
||||
}
|
||||
super(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
|
||||
if (device.isBusy()) {
|
||||
installActivity.setInfoText(device.getBusyTask());
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.getType() != DeviceType.MIBAND || !device.isInitialized()) {
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
helper.getFirmwareInfo().checkValid();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
installActivity.setInfoText(ex.getLocalizedMessage());
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion()));
|
||||
fwItem.setIcon(R.drawable.ic_device_miband);
|
||||
|
||||
if (!helper.isFirmwareGenerallyCompatibleWith(device)) {
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (helper.isSingleFirmware()) {
|
||||
builder.append(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion()));
|
||||
} else {
|
||||
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
|
||||
}
|
||||
|
||||
|
||||
if (helper.isFirmwareWhitelisted()) {
|
||||
builder.append(" ").append(mContext.getString(R.string.miband_firmware_known));
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version));
|
||||
// TODO: set a CHECK (OKAY) button
|
||||
} else {
|
||||
builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n")
|
||||
.append(mContext.getString(R.string.miband_firmware_suggest_whitelist, helper.getFirmwareVersion()));
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
|
||||
// TODO: set a UNKNOWN (question mark) button
|
||||
}
|
||||
installActivity.setInfoText(builder.toString());
|
||||
installActivity.setInstallItem(fwItem);
|
||||
installActivity.setInstallEnabled(true);
|
||||
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
|
||||
return new MiBandFWHelper(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartInstall(GBDevice device) {
|
||||
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return helper != null;
|
||||
protected boolean isSupportedDeviceType(GBDevice device) {
|
||||
return device.getType() == DeviceType.MIBAND;
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ public class MiBandPairingActivity extends GBActivity {
|
||||
|
||||
private void startPairing() {
|
||||
isPairing = true;
|
||||
message.setText(getString(R.string.miband_pairing, macAddress));
|
||||
message.setText(getString(R.string.pairing, macAddress));
|
||||
|
||||
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
|
||||
@ -209,20 +209,20 @@ public class MiBandPairingActivity extends GBActivity {
|
||||
protected void performBluetoothPair(BluetoothDevice device) {
|
||||
int bondState = device.getBondState();
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
GB.toast(getString(R.string.miband_pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
GB.toast(getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
performPair();
|
||||
return;
|
||||
}
|
||||
|
||||
bondingMacAddress = device.getAddress();
|
||||
if (bondState == BluetoothDevice.BOND_BONDING) {
|
||||
GB.toast(this, getString(R.string.miband_pairing_in_progress, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
|
||||
GB.toast(this, getString(R.string.pairing_in_progress, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
GB.toast(this, getString(R.string.miband_pairing_creating_bond_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
|
||||
GB.toast(this, getString(R.string.pairing_creating_bond_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.INFO);
|
||||
if (!device.createBond()) {
|
||||
GB.toast(this, getString(R.string.miband_pairing_unable_to_pair_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.ERROR);
|
||||
GB.toast(this, getString(R.string.pairing_unable_to_pair_with, device.getName(), bondingMacAddress), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_INCOMING_CALL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ADDRESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_FITNESS_GOAL;
|
||||
@ -44,8 +46,58 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT);
|
||||
setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DATEFORMAT);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
final Preference activateDisplayOnLift = findPreference(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
|
||||
activateDisplayOnLift.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GBApplication.deviceService().onSendConfiguration(PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
final Preference fitnessGoal = findPreference(PREF_MIBAND_FITNESS_GOAL);
|
||||
fitnessGoal.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GBApplication.deviceService().onSendConfiguration(PREF_MIBAND_FITNESS_GOAL);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* delayed execution so that the preferences are applied first
|
||||
*/
|
||||
private void invokeLater(Runnable runnable) {
|
||||
getListView().post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
final Preference developmentMiaddr = findPreference(PREF_MIBAND_ADDRESS);
|
||||
|
@ -1,16 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class MiBandSampleProvider extends AbstractSampleProvider<MiBandActivitySample> {
|
||||
public class MiBandSampleProvider extends AbstractMiBandSampleProvider {
|
||||
public static final int TYPE_DEEP_SLEEP = 4;
|
||||
public static final int TYPE_LIGHT_SLEEP = 5;
|
||||
public static final int TYPE_ACTIVITY = -1;
|
||||
@ -26,13 +21,15 @@ public class MiBandSampleProvider extends AbstractSampleProvider<MiBandActivityS
|
||||
// public static final byte TYPE_USER = 100;
|
||||
// public static final byte TYPE_WALKING = 1;
|
||||
|
||||
// maybe this should be configurable 256 seems way off, though.
|
||||
private final float movementDivisor = 180.0f; //256.0f;
|
||||
|
||||
public MiBandSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() {
|
||||
return SampleProvider.PROVIDER_MIBAND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(int rawType) {
|
||||
switch (rawType) {
|
||||
@ -68,39 +65,4 @@ public class MiBandSampleProvider extends AbstractSampleProvider<MiBandActivityS
|
||||
return TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity / movementDivisor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() {
|
||||
return SampleProvider.PROVIDER_MIBAND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<MiBandActivitySample, ?> getSampleDao() {
|
||||
return getSession().getMiBandActivitySampleDao();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return MiBandActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return MiBandActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return MiBandActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MiBandActivitySample createActivitySample() {
|
||||
return new MiBandActivitySample();
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ public class VibrationProfile {
|
||||
*
|
||||
* @param id the ID, used as preference key.
|
||||
* @param onOffSequence a sequence of alternating on and off durations, in milliseconds
|
||||
* @param repeat how ofoften the sequence shall be repeated
|
||||
* @param repeat how often the sequence shall be repeated
|
||||
*/
|
||||
public VibrationProfile(String id, int[] onOffSequence, short repeat) {
|
||||
this.id = id;
|
||||
|
@ -0,0 +1,73 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo;
|
||||
|
||||
public class MiBand2FWHelper extends AbstractMiBandFWHelper {
|
||||
private Mi2FirmwareInfo firmwareInfo;
|
||||
|
||||
public MiBand2FWHelper(Uri uri, Context context) throws IOException {
|
||||
super(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int version) {
|
||||
return Mi2FirmwareInfo.toVersion(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareVersion() {
|
||||
return firmwareInfo.getFirmwareVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmware2Version() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHumanFirmwareVersion2() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] getWhitelistedFirmwareVersions() {
|
||||
return Mi2FirmwareInfo.getWhitelistedVersions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
|
||||
return firmwareInfo.isGenerallyCompatibleWith(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleFirmware() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
|
||||
firmwareInfo = new Mi2FirmwareInfo(wholeFirmwareBytes);
|
||||
if (!firmwareInfo.isHeaderValid()) {
|
||||
throw new IllegalArgumentException("Not a Mi Band 2 firmware");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
firmwareInfo.checkValid();
|
||||
}
|
||||
|
||||
public Mi2FirmwareInfo getFirmwareInfo() {
|
||||
return firmwareInfo;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class MiBand2FWInstallHandler extends AbstractMiBandFWInstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBand2FWInstallHandler.class);
|
||||
|
||||
public MiBand2FWInstallHandler(Uri uri, Context context) {
|
||||
super(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
|
||||
return new MiBand2FWHelper(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSupportedDeviceType(GBDevice device) {
|
||||
return device.getType() == DeviceType.MIBAND2;
|
||||
}
|
||||
}
|
@ -181,7 +181,7 @@ public class PBWInstallHandler implements InstallHandler {
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
// always pretend it is valid, as we cant know yet about hw/fw version
|
||||
// always pretend it is valid, as we can't know yet about hw/fw version
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -232,7 +232,7 @@ public class PBWReader {
|
||||
byte[] tmp_buf = new byte[32];
|
||||
ByteBuffer buf = ByteBuffer.wrap(buffer);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.getLong(); // header, TODO: verifiy
|
||||
buf.getLong(); // header, TODO: verify
|
||||
buf.getShort(); // struct version, TODO: verify
|
||||
mSdkVersion = buf.getShort();
|
||||
mAppVersion = buf.getShort();
|
||||
|
@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class PebbleCoordinator extends AbstractDeviceCoordinator {
|
||||
@ -29,9 +30,12 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
return name != null && name.startsWith("Pebble");
|
||||
if (name != null && name.startsWith("Pebble")) {
|
||||
return DeviceType.PEBBLE;
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -41,9 +45,10 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
return PebblePairingActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPrimaryActivity() {
|
||||
return AppManagerActivity.class;
|
||||
}
|
||||
@ -105,7 +110,7 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
return PebbleUtils.hasHRM(device.getModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,242 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.pebble;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.query.Query;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.GBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class PebblePairingActivity extends GBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebblePairingActivity.class);
|
||||
private TextView message;
|
||||
private boolean isPairing;
|
||||
private boolean isLEPebble;
|
||||
private String macAddress;
|
||||
private BluetoothDevice mBtDevice;
|
||||
|
||||
private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
|
||||
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
LOG.debug("pairing activity: device changed: " + device);
|
||||
if (macAddress.equals(device.getAddress()) || macAddress.equals(device.getVolatileAddress())) {
|
||||
if (device.isInitialized()) {
|
||||
pairingFinished(true);
|
||||
} else if (device.isConnecting() || device.isInitializing()) {
|
||||
LOG.info("still connecting/initializing device...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver mBondingReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
LOG.info("Bond state changed: " + device + ", state: " + device.getBondState() + ", expected address: " + macAddress);
|
||||
if (macAddress != null && macAddress.equals(device.getAddress())) {
|
||||
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Bonded with " + device.getAddress());
|
||||
if (!isLEPebble) {
|
||||
performConnect(null);
|
||||
}
|
||||
} else if (bondState == BluetoothDevice.BOND_BONDING) {
|
||||
LOG.info("Bonding in progress with " + device.getAddress());
|
||||
} else if (bondState == BluetoothDevice.BOND_NONE) {
|
||||
LOG.info("Not bonded with " + device.getAddress() + ", attempting to connect anyway.");
|
||||
} else {
|
||||
LOG.warn("Unknown bond state for device " + device.getAddress() + ": " + bondState);
|
||||
pairingFinished(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pebble_pairing);
|
||||
|
||||
message = (TextView) findViewById(R.id.pebble_pair_message);
|
||||
Intent intent = getIntent();
|
||||
macAddress = intent.getStringExtra(DeviceCoordinator.EXTRA_DEVICE_MAC_ADDRESS);
|
||||
if (macAddress == null) {
|
||||
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
|
||||
returnToPairingActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
mBtDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddress);
|
||||
if (mBtDevice == null) {
|
||||
GB.toast(this, "No such Bluetooth Device: " + macAddress, Toast.LENGTH_LONG, GB.ERROR);
|
||||
returnToPairingActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
isLEPebble = mBtDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE;
|
||||
|
||||
GBDevice gbDevice = null;
|
||||
if (isLEPebble) {
|
||||
if (mBtDevice.getName().startsWith("Pebble-LE ") || mBtDevice.getName().startsWith("Pebble Time LE ")) {
|
||||
if (!GBApplication.getPrefs().getBoolean("pebble_force_le", false)) {
|
||||
GB.toast(this, "Please switch on \"Always prefer BLE\" option in Pebble settings before pairing you Pebble LE", Toast.LENGTH_LONG, GB.ERROR);
|
||||
returnToPairingActivity();
|
||||
return;
|
||||
}
|
||||
gbDevice = getMatchingParentDeviceFromDB(mBtDevice);
|
||||
if (gbDevice == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
startPairing(gbDevice);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
try {
|
||||
// just to be sure, remove the receivers -- might actually be already unregistered
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
|
||||
unregisterReceiver(mBondingReceiver);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// already unregistered, ignore
|
||||
}
|
||||
if (isPairing) {
|
||||
stopPairing();
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void startPairing(GBDevice gbDevice) {
|
||||
isPairing = true;
|
||||
message.setText(getString(R.string.pairing, macAddress));
|
||||
|
||||
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
|
||||
filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
registerReceiver(mBondingReceiver, filter);
|
||||
|
||||
performPair(gbDevice);
|
||||
}
|
||||
|
||||
private void pairingFinished(boolean pairedSuccessfully) {
|
||||
LOG.debug("pairingFinished: " + pairedSuccessfully);
|
||||
if (!isPairing) {
|
||||
// already gone?
|
||||
return;
|
||||
}
|
||||
|
||||
isPairing = false;
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mPairingReceiver);
|
||||
unregisterReceiver(mBondingReceiver);
|
||||
|
||||
if (pairedSuccessfully) {
|
||||
Intent intent = new Intent(this, ControlCenter.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
private void stopPairing() {
|
||||
// TODO
|
||||
isPairing = false;
|
||||
}
|
||||
|
||||
protected void performPair(GBDevice gbDevice) {
|
||||
int bondState = mBtDevice.getBondState();
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
GB.toast(getString(R.string.pairing_already_bonded, mBtDevice.getName(), mBtDevice.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDING) {
|
||||
GB.toast(this, getString(R.string.pairing_in_progress, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
GB.toast(this, getString(R.string.pairing_creating_bond_with, mBtDevice.getName(), macAddress), Toast.LENGTH_LONG, GB.INFO);
|
||||
GBApplication.deviceService().disconnect(); // just to make sure...
|
||||
|
||||
if (isLEPebble) {
|
||||
performConnect(gbDevice);
|
||||
} else {
|
||||
mBtDevice.createBond();
|
||||
}
|
||||
}
|
||||
|
||||
private void performConnect(GBDevice gbDevice) {
|
||||
if (gbDevice == null) {
|
||||
gbDevice = new GBDevice(mBtDevice.getAddress(), mBtDevice.getName(), DeviceType.PEBBLE);
|
||||
}
|
||||
GBApplication.deviceService().connect(gbDevice);
|
||||
}
|
||||
|
||||
private void returnToPairingActivity() {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
finish();
|
||||
}
|
||||
|
||||
private GBDevice getMatchingParentDeviceFromDB(BluetoothDevice btDevice) {
|
||||
String expectedSuffix = btDevice.getName();
|
||||
expectedSuffix = expectedSuffix.replace("Pebble-LE ", "");
|
||||
expectedSuffix = expectedSuffix.replace("Pebble Time LE ", "");
|
||||
expectedSuffix = expectedSuffix.substring(0, 2) + ":" + expectedSuffix.substring(2);
|
||||
LOG.info("will try to find a Pebble with BT address suffix " + expectedSuffix);
|
||||
GBDevice gbDevice = null;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
DeviceDao deviceDao = session.getDeviceDao();
|
||||
Query<Device> query = deviceDao.queryBuilder().where(DeviceDao.Properties.Type.eq(1), DeviceDao.Properties.Identifier.like("%" + expectedSuffix)).build();
|
||||
List<Device> devices = query.list();
|
||||
if (devices.size() == 0) {
|
||||
GB.toast("Please pair your non-LE Pebble before pairing the LE one", Toast.LENGTH_SHORT, GB.INFO);
|
||||
returnToPairingActivity();
|
||||
return null;
|
||||
} else if (devices.size() > 1) {
|
||||
GB.toast("Can not match this Pebble LE to a unique device", Toast.LENGTH_SHORT, GB.INFO);
|
||||
returnToPairingActivity();
|
||||
return null;
|
||||
}
|
||||
DeviceHelper deviceHelper = DeviceHelper.getInstance();
|
||||
gbDevice = deviceHelper.toGBDevice(devices.get(0));
|
||||
gbDevice.setVolatileAddress(btDevice.getAddress());
|
||||
} catch (Exception e) {
|
||||
GB.toast("Error retrieving devices from database", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
returnToPairingActivity();
|
||||
return null;
|
||||
}
|
||||
return gbDevice;
|
||||
}
|
||||
}
|
@ -20,9 +20,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
return name != null && name.startsWith("Vibratissimo");
|
||||
if (name != null && name.startsWith("Vibratissimo")) {
|
||||
return DeviceType.VIBRATISSIMO;
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -74,12 +74,14 @@ public abstract class AbstractActivitySample implements ActivitySample {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
int kind = getProvider() != null ? getKind() : ActivitySample.NOT_MEASURED;
|
||||
float intensity = getProvider() != null ? getIntensity() : ActivitySample.NOT_MEASURED;
|
||||
return getClass().getSimpleName() + "{" +
|
||||
"timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimeStamp(getTimestamp())) +
|
||||
", intensity=" + getIntensity() +
|
||||
", intensity=" + intensity +
|
||||
", steps=" + getSteps() +
|
||||
", heartrate=" + getHeartRate() +
|
||||
", type=" + getKind() +
|
||||
", type=" + kind +
|
||||
", userId=" + getUserId() +
|
||||
", deviceId=" + getDeviceId() +
|
||||
'}';
|
||||
|
@ -1,11 +1,17 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
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.support.v4.app.ActivityCompat;
|
||||
|
||||
import net.e175.klaus.solarpositioning.DeltaT;
|
||||
import net.e175.klaus.solarpositioning.SPA;
|
||||
@ -64,6 +70,21 @@ public class AlarmReceiver extends BroadcastReceiver {
|
||||
float latitude = prefs.getFloat("location_latitude", 0);
|
||||
float longitude = prefs.getFloat("location_longitude", 0);
|
||||
LOG.info("got longitude/latitude from preferences: " + latitude + "/" + longitude);
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
|
||||
prefs.getBoolean("use_updated_location_if_available", false)) {
|
||||
LocationManager locationManager = (LocationManager) context.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) {
|
||||
latitude = (float) lastKnownLocation.getLatitude();
|
||||
longitude = (float) lastKnownLocation.getLongitude();
|
||||
LOG.info("got longitude/latitude from last known location: " + latitude + "/" + longitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
GregorianCalendar[] sunriseTransitSetTomorrow = SPA.calculateSunriseTransitSet(dateTimeTomorrow, latitude, longitude, DeltaT.estimate(dateTimeTomorrow));
|
||||
|
||||
CalendarEventSpec calendarEventSpec = new CalendarEventSpec();
|
||||
|
@ -57,7 +57,7 @@ public class K9Receiver extends BroadcastReceiver {
|
||||
notificationSpec.type = NotificationType.GENERIC_EMAIL;
|
||||
|
||||
/*
|
||||
* there seems to be no way to specify the the uri in the where clause.
|
||||
* there seems to be no way to specify the uri in the where clause.
|
||||
* If we do so, we just get the newest message, not the one requested.
|
||||
* So, we will just search our message and match the uri manually.
|
||||
* It should be the first one returned by the query in most cases,
|
||||
|
@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
@ -189,16 +190,19 @@ public class NotificationListener extends NotificationListenerService {
|
||||
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
|
||||
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
if (powermanager.isScreenOn()) {
|
||||
LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//don't forward group summary notifications to the wearable, they are meant for the android device only
|
||||
if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) {
|
||||
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
|
||||
LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -211,6 +215,7 @@ public class NotificationListener extends NotificationListenerService {
|
||||
source.equals("com.android.systemui") ||
|
||||
source.equals("com.android.dialer") ||
|
||||
source.equals("com.cyanogenmod.eleven")) {
|
||||
LOG.info("Not forwarding notification, is a system event");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -231,6 +236,7 @@ public class NotificationListener extends NotificationListenerService {
|
||||
}
|
||||
|
||||
if (GBApplication.blacklist != null && GBApplication.blacklist.contains(source)) {
|
||||
LOG.info("Not forwarding notification, application is blacklisted");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -250,48 +256,7 @@ public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
boolean preferBigText = false;
|
||||
|
||||
switch (source) {
|
||||
case "org.mariotaku.twidere":
|
||||
case "com.twitter.android":
|
||||
case "org.andstatus.app":
|
||||
case "org.mustard.android":
|
||||
notificationSpec.type = NotificationType.TWITTER;
|
||||
break;
|
||||
case "com.fsck.k9":
|
||||
case "com.android.email":
|
||||
notificationSpec.type = NotificationType.GENERIC_EMAIL;
|
||||
preferBigText = true;
|
||||
break;
|
||||
case "com.moez.QKSMS":
|
||||
case "com.android.mms":
|
||||
case "com.android.messaging":
|
||||
case "com.sonyericsson.conversations":
|
||||
case "org.smssecure.smssecure":
|
||||
notificationSpec.type = NotificationType.GENERIC_SMS;
|
||||
break;
|
||||
case "eu.siacs.conversations":
|
||||
notificationSpec.type = NotificationType.CONVERSATIONS;
|
||||
break;
|
||||
case "org.thoughtcrime.securesms":
|
||||
notificationSpec.type = NotificationType.SIGNAL;
|
||||
break;
|
||||
case "org.telegram.messenger":
|
||||
notificationSpec.type = NotificationType.TELEGRAM;
|
||||
break;
|
||||
case "me.zeeroooo.materialfb":
|
||||
case "it.rignanese.leo.slimfacebook":
|
||||
case "me.jakelane.wrapperforfacebook":
|
||||
case "com.facebook.katana":
|
||||
case "org.indywidualni.fblite":
|
||||
notificationSpec.type = NotificationType.FACEBOOK;
|
||||
break;
|
||||
case "com.facebook.orca":
|
||||
notificationSpec.type = NotificationType.FACEBOOK_MESSENGER;
|
||||
break;
|
||||
default:
|
||||
notificationSpec.type = NotificationType.UNKNOWN;
|
||||
break;
|
||||
}
|
||||
notificationSpec.type = AppNotificationType.getInstance().get(source);
|
||||
|
||||
LOG.info("Processing notification from source " + source);
|
||||
|
||||
|
@ -45,8 +45,10 @@ public class GBDevice implements Parcelable {
|
||||
private static final String DEVINFO_FW_VER = "FW: ";
|
||||
private static final String DEVINFO_HR_VER = "HR: ";
|
||||
private static final String DEVINFO_ADDR = "ADDR: ";
|
||||
private static final String DEVINFO_ADDR2 = "ADDR2: ";
|
||||
private String mName;
|
||||
private final String mAddress;
|
||||
private String mVolatileAddress;
|
||||
private final DeviceType mDeviceType;
|
||||
private String mFirmwareVersion;
|
||||
private String mFirmwareVersion2;
|
||||
@ -60,7 +62,12 @@ public class GBDevice implements Parcelable {
|
||||
private List<ItemWithDetails> mDeviceInfos;
|
||||
|
||||
public GBDevice(String address, String name, DeviceType deviceType) {
|
||||
this(address, null, name, deviceType);
|
||||
}
|
||||
|
||||
public GBDevice(String address, String address2, String name, DeviceType deviceType) {
|
||||
mAddress = address;
|
||||
mVolatileAddress = address2;
|
||||
mName = (name != null) ? name : mAddress;
|
||||
mDeviceType = deviceType;
|
||||
validate();
|
||||
@ -69,6 +76,7 @@ public class GBDevice implements Parcelable {
|
||||
private GBDevice(Parcel in) {
|
||||
mName = in.readString();
|
||||
mAddress = in.readString();
|
||||
mVolatileAddress = in.readString();
|
||||
mDeviceType = DeviceType.values()[in.readInt()];
|
||||
mFirmwareVersion = in.readString();
|
||||
mFirmwareVersion2 = in.readString();
|
||||
@ -88,6 +96,7 @@ public class GBDevice implements Parcelable {
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mName);
|
||||
dest.writeString(mAddress);
|
||||
dest.writeString(mVolatileAddress);
|
||||
dest.writeInt(mDeviceType.ordinal());
|
||||
dest.writeString(mFirmwareVersion);
|
||||
dest.writeString(mFirmwareVersion2);
|
||||
@ -123,6 +132,10 @@ public class GBDevice implements Parcelable {
|
||||
return mAddress;
|
||||
}
|
||||
|
||||
public String getVolatileAddress() {
|
||||
return mVolatileAddress;
|
||||
}
|
||||
|
||||
public String getFirmwareVersion() {
|
||||
return mFirmwareVersion;
|
||||
}
|
||||
@ -142,6 +155,10 @@ public class GBDevice implements Parcelable {
|
||||
mFirmwareVersion2 = firmwareVersion2;
|
||||
}
|
||||
|
||||
public void setVolatileAddress(String volatileAddress) {
|
||||
mVolatileAddress = volatileAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specific model/hardware revision of this device.
|
||||
* This information is not always available, typically only when the device is initialized
|
||||
@ -240,7 +257,7 @@ public class GBDevice implements Parcelable {
|
||||
}
|
||||
|
||||
/**
|
||||
* for simplicity the user wont see all internal states, just connecting -> connected
|
||||
* for simplicity the user won't see all internal states, just connecting -> connected
|
||||
* instead of connecting->connected->initializing->initialized
|
||||
* Set simple to true to get this behavior.
|
||||
*/
|
||||
@ -416,6 +433,9 @@ public class GBDevice implements Parcelable {
|
||||
if (mAddress != null) {
|
||||
result.add(new GenericItem(DEVINFO_ADDR, mAddress));
|
||||
}
|
||||
if (mVolatileAddress != null) {
|
||||
result.add(new GenericItem(DEVINFO_ADDR2, mVolatileAddress));
|
||||
}
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
@ -4,17 +4,22 @@ import android.bluetooth.BluetoothDevice;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
|
||||
/**
|
||||
* A device candidate is a Bluetooth device that is not yet managed by
|
||||
@ -26,21 +31,25 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
|
||||
private final BluetoothDevice device;
|
||||
private final short rssi;
|
||||
private final ParcelUuid[] serviceUuds;
|
||||
private DeviceType deviceType = DeviceType.UNKNOWN;
|
||||
|
||||
public GBDeviceCandidate(BluetoothDevice device, short rssi) {
|
||||
public GBDeviceCandidate(BluetoothDevice device, short rssi, ParcelUuid[] serviceUuds) {
|
||||
this.device = device;
|
||||
this.rssi = rssi;
|
||||
this.serviceUuds = mergeServiceUuids(serviceUuds, device.getUuids());
|
||||
}
|
||||
|
||||
private GBDeviceCandidate(Parcel in) {
|
||||
device = in.readParcelable(getClass().getClassLoader());
|
||||
if (device == null) {
|
||||
throw new IllegalStateException("Unable to read state from Parcel");
|
||||
}
|
||||
rssi = (short) in.readInt();
|
||||
deviceType = DeviceType.valueOf(in.readString());
|
||||
|
||||
if (device == null || deviceType == null) {
|
||||
throw new IllegalStateException("Unable to read state from Parcel");
|
||||
}
|
||||
ParcelUuid[] uuids = AndroidUtils.toParcelUUids(in.readParcelableArray(getClass().getClassLoader()));
|
||||
serviceUuds = mergeServiceUuids(uuids, device.getUuids());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -48,12 +57,29 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
dest.writeParcelable(device, 0);
|
||||
dest.writeInt(rssi);
|
||||
dest.writeString(deviceType.name());
|
||||
dest.writeArray(serviceUuds);
|
||||
}
|
||||
|
||||
public static final Creator<GBDeviceCandidate> CREATOR = new Creator<GBDeviceCandidate>() {
|
||||
@Override
|
||||
public GBDeviceCandidate createFromParcel(Parcel in) {
|
||||
return new GBDeviceCandidate(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceCandidate[] newArray(int size) {
|
||||
return new GBDeviceCandidate[size];
|
||||
}
|
||||
};
|
||||
|
||||
public BluetoothDevice getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public void setDeviceType(DeviceType type) {
|
||||
deviceType = type;
|
||||
}
|
||||
|
||||
public DeviceType getDeviceType() {
|
||||
return deviceType;
|
||||
}
|
||||
@ -62,9 +88,25 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
return device != null ? device.getAddress() : GBApplication.getContext().getString(R.string._unknown_);
|
||||
}
|
||||
|
||||
private ParcelUuid[] mergeServiceUuids(ParcelUuid[] serviceUuds, ParcelUuid[] deviceUuids) {
|
||||
Set<ParcelUuid> uuids = new HashSet<>();
|
||||
if (serviceUuds != null) {
|
||||
uuids.addAll(Arrays.asList(serviceUuds));
|
||||
}
|
||||
if (deviceUuids != null) {
|
||||
uuids.addAll(Arrays.asList(deviceUuids));
|
||||
}
|
||||
return uuids.toArray(new ParcelUuid[0]);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ParcelUuid[] getServiceUuids() {
|
||||
return serviceUuds;
|
||||
}
|
||||
|
||||
public boolean supportsService(UUID aService) {
|
||||
ParcelUuid[] uuids = device.getUuids();
|
||||
if (uuids == null) {
|
||||
ParcelUuid[] uuids = getServiceUuids();
|
||||
if (uuids.length == 0) {
|
||||
LOG.warn("no cached services available for " + this);
|
||||
return false;
|
||||
}
|
||||
|
@ -281,6 +281,13 @@ public class GBDeviceService implements DeviceService {
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
Intent intent = createIntent().setAction(ACTION_SEND_CONFIGURATION)
|
||||
.putExtra(EXTRA_CONFIG, config);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
Intent intent = createIntent().setAction(ACTION_TEST_NEW_FUNCTION);
|
||||
|
@ -0,0 +1,58 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class AppNotificationType extends HashMap<String, NotificationType> {
|
||||
|
||||
private static AppNotificationType _instance;
|
||||
|
||||
public static AppNotificationType getInstance() {
|
||||
if(_instance == null) {
|
||||
return (_instance = new AppNotificationType());
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
|
||||
private AppNotificationType() {
|
||||
// Generic Email
|
||||
put("com.fsck.k9", NotificationType.GENERIC_EMAIL);
|
||||
put("com.android.email", NotificationType.GENERIC_EMAIL);
|
||||
|
||||
// Generic SMS
|
||||
put("com.moez.QKSMS", NotificationType.GENERIC_SMS);
|
||||
put("com.android.mms", NotificationType.GENERIC_SMS);
|
||||
put("com.android.messaging", NotificationType.GENERIC_SMS);
|
||||
put("com.sonyericsson.conversations", NotificationType.GENERIC_SMS);
|
||||
put("org.smssecure.smssecure", NotificationType.GENERIC_SMS);
|
||||
|
||||
// Conversations
|
||||
put("eu.siacs.conversations", NotificationType.CONVERSATIONS);
|
||||
|
||||
// Signal
|
||||
put("org.thoughtcrime.securesms", NotificationType.SIGNAL);
|
||||
|
||||
// Telegram
|
||||
put("org.telegram.messenger", NotificationType.TELEGRAM);
|
||||
|
||||
// Twitter
|
||||
put("org.mariotaku.twidere", NotificationType.TWITTER);
|
||||
put("com.twitter.android", NotificationType.TWITTER);
|
||||
put("org.andstatus.app", NotificationType.TWITTER);
|
||||
put("org.mustard.android", NotificationType.TWITTER);
|
||||
|
||||
// Facebook
|
||||
put("me.zeeroooo.materialfb", NotificationType.FACEBOOK);
|
||||
put("it.rignanese.leo.slimfacebook", NotificationType.FACEBOOK);
|
||||
put("me.jakelane.wrapperforfacebook", NotificationType.FACEBOOK);
|
||||
put("com.facebook.katana", NotificationType.FACEBOOK);
|
||||
put("org.indywidualni.fblite", NotificationType.FACEBOOK);
|
||||
|
||||
// Facebook Messenger
|
||||
put("com.facebook.orca", NotificationType.FACEBOOK_MESSENGER);
|
||||
|
||||
// WhatsApp
|
||||
put("com.whatsapp", NotificationType.WHATSAPP);
|
||||
}
|
||||
|
||||
}
|
@ -48,6 +48,7 @@ public interface DeviceService extends EventHandler {
|
||||
String ACTION_HEARTRATE_MEASUREMENT = PREFIX + ".action.hr_measurement";
|
||||
String ACTION_ADD_CALENDAREVENT = PREFIX + ".action.add_calendarevent";
|
||||
String ACTION_DELETE_CALENDAREVENT = PREFIX + ".action.delete_calendarevent";
|
||||
String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration";
|
||||
String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function";
|
||||
String EXTRA_DEVICE_ADDRESS = "device_address";
|
||||
String EXTRA_NOTIFICATION_BODY = "notification_body";
|
||||
@ -80,6 +81,7 @@ public interface DeviceService extends EventHandler {
|
||||
String EXTRA_APP_START = "app_start";
|
||||
String EXTRA_APP_CONFIG = "app_config";
|
||||
String EXTRA_URI = "uri";
|
||||
String EXTRA_CONFIG = "config";
|
||||
String EXTRA_ALARMS = "alarms";
|
||||
String EXTRA_PERFORM_PAIR = "perform_pair";
|
||||
String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps";
|
||||
|
@ -12,6 +12,7 @@ public enum DeviceType {
|
||||
MIBAND(10),
|
||||
MIBAND2(11),
|
||||
VIBRATISSIMO(20),
|
||||
LIVEVIEW(30),
|
||||
TEST(1000);
|
||||
|
||||
private final int key;
|
||||
@ -24,6 +25,10 @@ public enum DeviceType {
|
||||
return key;
|
||||
}
|
||||
|
||||
public boolean isSupported() {
|
||||
return this != UNKNOWN;
|
||||
}
|
||||
|
||||
public static DeviceType fromKey(int key) {
|
||||
for (DeviceType type : values()) {
|
||||
if (type.key == key) {
|
||||
|
@ -1,24 +1,35 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID;
|
||||
|
||||
public enum NotificationType {
|
||||
|
||||
UNKNOWN,
|
||||
UNKNOWN(PebbleIconID.NOTIFICATION_GENERIC, PebbleColor.Red),
|
||||
|
||||
CONVERSATIONS,
|
||||
GENERIC_EMAIL,
|
||||
GENERIC_NAVIGATION,
|
||||
GENERIC_SMS,
|
||||
FACEBOOK,
|
||||
FACEBOOK_MESSENGER,
|
||||
SIGNAL,
|
||||
TWITTER,
|
||||
TELEGRAM;
|
||||
CONVERSATIONS(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.Inchworm),
|
||||
GENERIC_EMAIL(PebbleIconID.GENERIC_EMAIL, PebbleColor.JaegerGreen),
|
||||
GENERIC_NAVIGATION(PebbleIconID.LOCATION, PebbleColor.Orange),
|
||||
GENERIC_SMS(PebbleIconID.GENERIC_SMS, PebbleColor.VividViolet),
|
||||
FACEBOOK(PebbleIconID.NOTIFICATION_FACEBOOK, PebbleColor.Liberty),
|
||||
FACEBOOK_MESSENGER(PebbleIconID.NOTIFICATION_FACEBOOK_MESSENGER, PebbleColor.VeryLightBlue),
|
||||
SIGNAL(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.BlueMoon),
|
||||
TWITTER(PebbleIconID.NOTIFICATION_TWITTER, PebbleColor.BlueMoon),
|
||||
TELEGRAM(PebbleIconID.NOTIFICATION_TELEGRAM, PebbleColor.PictonBlue),
|
||||
WHATSAPP(PebbleIconID.NOTIFICATION_WHATSAPP, PebbleColor.MayGreen);
|
||||
|
||||
public int icon;
|
||||
public byte color;
|
||||
|
||||
NotificationType(int icon, byte color) {
|
||||
this.icon = icon;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the enum constant as a fixed String value, e.g. to be used
|
||||
* as preference key. In case the keys are ever changed, this method
|
||||
* may be used to bring backward compatibility.
|
||||
* @return
|
||||
*/
|
||||
public String getFixedValue() {
|
||||
return name().toLowerCase();
|
||||
@ -37,6 +48,7 @@ public enum NotificationType {
|
||||
case FACEBOOK_MESSENGER:
|
||||
case SIGNAL:
|
||||
case TELEGRAM:
|
||||
case WHATSAPP:
|
||||
return "generic_chat";
|
||||
case UNKNOWN:
|
||||
default:
|
||||
|
@ -72,6 +72,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RE
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_CONFIGURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETCANNEDMESSAGES;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICSTATE;
|
||||
@ -96,6 +97,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAL
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CALL_PHONENUMBER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CANNEDMESSAGES_TYPE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_DEVICE_ADDRESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM;
|
||||
@ -167,20 +169,25 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
boolean enableReceivers = mDeviceSupport != null && (mDeviceSupport.useAutoConnect() || mGBDevice.isInitialized());
|
||||
setReceiversEnableState(enableReceivers);
|
||||
GB.updateNotification(mGBDevice.getName() + " " + mGBDevice.getStateString(), mGBDevice.isInitialized(), context);
|
||||
if (device.isInitialized() && (device.getType() != DeviceType.VIBRATISSIMO)) {
|
||||
|
||||
if (device.isInitialized()) {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
if (DBHelper.findDevice(device, session) == null) {
|
||||
boolean askForDBMigration = false;
|
||||
if (DBHelper.findDevice(device, session) == null && device.getType() != DeviceType.VIBRATISSIMO && (device.getType() != DeviceType.LIVEVIEW)) {
|
||||
askForDBMigration = true;
|
||||
}
|
||||
DBHelper.getDevice(device, session); // implicitly creates the device in database if not present, and updates device attributes
|
||||
if (askForDBMigration) {
|
||||
DBHelper dbHelper = new DBHelper(context);
|
||||
if (dbHelper.getOldActivityDatabaseHandler() != null) {
|
||||
DBHelper.getDevice(device, session); // implicitly creates it :P
|
||||
Intent startIntent = new Intent(context, OnboardingActivity.class);
|
||||
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
} catch (Exception _ignore) {
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -424,11 +431,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
break;
|
||||
case ACTION_SETMUSICSTATE:
|
||||
MusicStateSpec stateSpec = new MusicStateSpec();
|
||||
stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte)0);
|
||||
stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte)0);
|
||||
stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte) 0);
|
||||
stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte) 0);
|
||||
stateSpec.position = intent.getIntExtra(EXTRA_MUSIC_POSITION, 0);
|
||||
stateSpec.playRate = intent.getIntExtra(EXTRA_MUSIC_RATE, 0);
|
||||
stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte)0);
|
||||
stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte) 0);
|
||||
mDeviceSupport.onSetMusicState(stateSpec);
|
||||
break;
|
||||
case ACTION_REQUEST_APPINFO:
|
||||
@ -485,6 +492,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
mDeviceSupport.onEnableRealtimeHeartRateMeasurement(enable);
|
||||
break;
|
||||
}
|
||||
case ACTION_SEND_CONFIGURATION: {
|
||||
String config = intent.getStringExtra(EXTRA_CONFIG);
|
||||
mDeviceSupport.onSendConfiguration(config);
|
||||
break;
|
||||
}
|
||||
case ACTION_TEST_NEW_FUNCTION: {
|
||||
mDeviceSupport.onTestNewFunction();
|
||||
break;
|
||||
@ -632,7 +644,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
|
||||
setDeviceSupport(null);
|
||||
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops
|
||||
nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification won't be cancelled when service stops
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,9 +9,9 @@ import java.util.EnumSet;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
|
||||
@ -93,6 +93,9 @@ public class DeviceSupportFactory {
|
||||
case VIBRATISSIMO:
|
||||
deviceSupport = new ServiceDeviceSupport(new VibratissimoSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case LIVEVIEW:
|
||||
deviceSupport = new ServiceDeviceSupport(new LiveviewSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
}
|
||||
if (deviceSupport != null) {
|
||||
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
|
||||
|
@ -320,6 +320,14 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
delegate.onDeleteCalendarEvent(type, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
if (checkBusy("send configuration: " + config)) {
|
||||
return;
|
||||
}
|
||||
delegate.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
if (checkBusy("test new function event")) {
|
||||
|
@ -59,7 +59,7 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLEDeviceSupport>
|
||||
* Subclasses must implement this. When invoked, #prePerform() returned
|
||||
* successfully.
|
||||
* Note that subclasses HAVE TO call #operationFinished() when the entire
|
||||
* opreation is done (successful or not).
|
||||
* operation is done (successful or not).
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@ -67,7 +67,7 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLEDeviceSupport>
|
||||
|
||||
/**
|
||||
* You MUST call this method when the operation has finished, either
|
||||
* successfull or unsuccessfully.
|
||||
* successfully or unsuccessfully.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
|
@ -52,6 +52,42 @@ public class BLETypeConversions {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to calendarToRawBytes, but only up to (and including) the MINUTES.
|
||||
* @param timestamp
|
||||
* @param honorDeviceTimeOffset
|
||||
* @return
|
||||
*/
|
||||
public static byte[] shortCalendarToRawBytes(Calendar timestamp, boolean honorDeviceTimeOffset) {
|
||||
|
||||
// The mi-band device currently records sleep
|
||||
// only if it happens after 10pm and before 7am.
|
||||
// The offset is used to trick the device to record sleep
|
||||
// in non-standard hours.
|
||||
// If you usually sleep, say, from 6am to 2pm, set the
|
||||
// shift to -8, so at 6am the device thinks it's still 10pm
|
||||
// of the day before.
|
||||
if (honorDeviceTimeOffset) {
|
||||
int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours();
|
||||
if (offsetInHours != 0) {
|
||||
timestamp.add(Calendar.HOUR_OF_DAY, offsetInHours);
|
||||
}
|
||||
}
|
||||
|
||||
// MiBand2:
|
||||
// year,year,month,dayofmonth,hour,minute
|
||||
|
||||
byte[] year = fromUint16(timestamp.get(Calendar.YEAR));
|
||||
return new byte[] {
|
||||
year[0],
|
||||
year[1],
|
||||
fromUint8(timestamp.get(Calendar.MONTH) + 1),
|
||||
fromUint8(timestamp.get(Calendar.DATE)),
|
||||
fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)),
|
||||
fromUint8(timestamp.get(Calendar.MINUTE))
|
||||
};
|
||||
}
|
||||
|
||||
private static int getMiBand2TimeZone(int rawOffset) {
|
||||
int offsetMinutes = rawOffset / 1000 / 60;
|
||||
rawOffset = offsetMinutes < 0 ? -1 : 1;
|
||||
@ -82,11 +118,11 @@ public class BLETypeConversions {
|
||||
int year = toUint16(value[0], value[1]);
|
||||
GregorianCalendar timestamp = new GregorianCalendar(
|
||||
year,
|
||||
value[2],
|
||||
value[3],
|
||||
value[4],
|
||||
value[5],
|
||||
value[6]
|
||||
(value[2] & 0xff) - 1,
|
||||
value[3] & 0xff,
|
||||
value[4] & 0xff,
|
||||
value[5] & 0xff,
|
||||
value[6] & 0xff
|
||||
);
|
||||
|
||||
if (honorDeviceTimeOffset) {
|
||||
@ -103,7 +139,7 @@ public class BLETypeConversions {
|
||||
}
|
||||
|
||||
public static int toUint16(byte... bytes) {
|
||||
return bytes[0] | (bytes[1] << 8);
|
||||
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8);
|
||||
}
|
||||
|
||||
public static byte[] fromUint16(int value) {
|
||||
@ -112,6 +148,24 @@ public class BLETypeConversions {
|
||||
(byte) ((value >> 8) & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] fromUint24(int value) {
|
||||
return new byte[] {
|
||||
(byte) (value & 0xff),
|
||||
(byte) ((value >> 8) & 0xff),
|
||||
(byte) ((value >> 16) & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
public static byte[] fromUint32(int value) {
|
||||
return new byte[] {
|
||||
(byte) (value & 0xff),
|
||||
(byte) ((value >> 8) & 0xff),
|
||||
(byte) ((value >> 16) & 0xff),
|
||||
(byte) ((value >> 24) & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
public static byte fromUint8(int value) {
|
||||
return (byte) (value & 0xff);
|
||||
}
|
||||
@ -156,7 +210,7 @@ public class BLETypeConversions {
|
||||
|
||||
/**
|
||||
* https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.dst_offset.xml
|
||||
* @param Calendar
|
||||
* @param now
|
||||
* @return the DST offset for the given time; 0 if none; 255 if unknown
|
||||
*/
|
||||
public static byte mapDstOffset(Calendar now) {
|
||||
|
@ -25,7 +25,7 @@ public abstract class BtLEAction {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this actions expects an (async) result which must
|
||||
* Returns true if this action expects an (async) result which must
|
||||
* be waited for, before continuing with other actions.
|
||||
* <p/>
|
||||
* This is needed because the current Bluedroid stack can only deal
|
||||
|
@ -48,7 +48,7 @@ public final class BtLEQueue {
|
||||
private final InternalGattCallback internalGattCallback;
|
||||
private boolean mAutoReconnect;
|
||||
|
||||
private Thread dispatchThread = new Thread("GadgetBridge GATT Dispatcher") {
|
||||
private Thread dispatchThread = new Thread("Gadgetbridge GATT Dispatcher") {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
@ -148,7 +148,7 @@ public final class BtLEQueue {
|
||||
}
|
||||
synchronized (mGattMonitor) {
|
||||
if (mBluetoothGatt != null) {
|
||||
// Tribal knowledge says you're better off not reusing existing BlueoothGatt connections,
|
||||
// Tribal knowledge says you're better off not reusing existing BluetoothGatt connections,
|
||||
// so create a new one.
|
||||
LOG.info("connect() requested -- disconnecting previous connection: " + mGbDevice.getName());
|
||||
disconnect();
|
||||
|
@ -9,7 +9,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.UUID_DESCRIPTOR_GATT_CLIENT_CHARACTERISTIC_CONFIGURATION;
|
||||
|
||||
@ -20,7 +19,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattDescriptor.U
|
||||
*/
|
||||
public class NotifyAction extends BtLEAction {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TransactionBuilder.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NotifyAction.class);
|
||||
protected final boolean enableFlag;
|
||||
private boolean hasWrittenDescriptor = true;
|
||||
|
||||
@ -49,7 +48,7 @@ public class NotifyAction extends BtLEAction {
|
||||
hasWrittenDescriptor = false;
|
||||
}
|
||||
} else {
|
||||
LOG.warn("sleep descriptor null");
|
||||
LOG.warn("Descriptor CLIENT_CHARACTERISTIC_CONFIGURATION for characteristic " + getCharacteristic().getUuid() + " is null");
|
||||
hasWrittenDescriptor = false;
|
||||
}
|
||||
} else {
|
||||
|
@ -4,6 +4,10 @@ import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
|
||||
/**
|
||||
@ -12,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
* {@link BluetoothGattCallback}
|
||||
*/
|
||||
public class WriteAction extends BtLEAction {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class);
|
||||
|
||||
private final byte[] value;
|
||||
|
||||
@ -24,7 +29,7 @@ public class WriteAction extends BtLEAction {
|
||||
public boolean run(BluetoothGatt gatt) {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic();
|
||||
int properties = characteristic.getProperties();
|
||||
//TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this yelds to timing issues
|
||||
//TODO: expectsResult should return false if PROPERTY_WRITE_NO_RESPONSE is true, but this leads to timing issues
|
||||
if ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0 || ((properties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0)) {
|
||||
return writeValue(gatt, characteristic, value);
|
||||
}
|
||||
@ -32,6 +37,9 @@ public class WriteAction extends BtLEAction {
|
||||
}
|
||||
|
||||
protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("writing to characteristic: " + characteristic.getUuid() + ": " + Logging.formatBytes(value));
|
||||
}
|
||||
if (characteristic.setValue(value)) {
|
||||
return gatt.writeCharacteristic(characteristic);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public enum AlertCategory {
|
||||
|
||||
/**
|
||||
* Returns the numerical ID value of this category
|
||||
* To be used as uin8 value
|
||||
* To be used as uint8 value
|
||||
* @return the uint8 value for this category
|
||||
*/
|
||||
public int getId() {
|
||||
|
@ -17,7 +17,7 @@ public class SupportedNewAlertCategory {
|
||||
|
||||
/**
|
||||
* Returns the numerical ID value of this category
|
||||
* To be used as uin8 value
|
||||
* To be used as uint8 value
|
||||
* @return the uint8 value for this category
|
||||
*/
|
||||
public int getId() {
|
||||
|
@ -7,7 +7,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate;
|
||||
*/
|
||||
public enum BodySensorLocation {
|
||||
Other(0),
|
||||
Checst(1),
|
||||
Chest(1),
|
||||
Wrist(2),
|
||||
Finger(3),
|
||||
Hand(4),
|
||||
|
@ -44,12 +44,6 @@ public class HeartRateProfile<T extends AbstractBTLEDeviceSupport> extends Abstr
|
||||
|
||||
}
|
||||
|
||||
// TODO: I didn't find anything in the spec to request heart rate readings, so probably this
|
||||
// should be done in a device specific way.
|
||||
public void requestHeartRateMeasurement(TransactionBuilder builder) {
|
||||
writeToControlPoint(new byte[] { 0x15, 0x02, 0x01}, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
|
||||
@ -61,7 +55,7 @@ public class HeartRateProfile<T extends AbstractBTLEDeviceSupport> extends Abstr
|
||||
format = BluetoothGattCharacteristic.FORMAT_UINT8;
|
||||
}
|
||||
final int heartRate = characteristic.getIntValue(format, 1);
|
||||
GB.toast(getContext(), "Heart rate: " + heartRate, Toast.LENGTH_LONG, GB.INFO);
|
||||
LOG.info("Heart rate: " + heartRate, Toast.LENGTH_LONG, GB.INFO);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -0,0 +1,220 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.Context;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class LiveviewIoThread extends GBDeviceIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LiveviewIoThread.class);
|
||||
|
||||
private static final UUID SERIAL = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
private final LiveviewProtocol mLiveviewProtocol;
|
||||
private final LiveviewSupport mLiveviewSupport;
|
||||
|
||||
|
||||
private BluetoothAdapter mBtAdapter = null;
|
||||
private BluetoothSocket mBtSocket = null;
|
||||
private InputStream mInStream = null;
|
||||
private OutputStream mOutStream = null;
|
||||
private boolean mQuit = false;
|
||||
|
||||
@Override
|
||||
public void quit() {
|
||||
mQuit = true;
|
||||
if (mBtSocket != null) {
|
||||
try {
|
||||
mBtSocket.close();
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mIsConnected = false;
|
||||
|
||||
|
||||
public LiveviewIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol lvProtocol, LiveviewSupport lvSupport, BluetoothAdapter lvBtAdapter) {
|
||||
super(gbDevice, context);
|
||||
mLiveviewProtocol = (LiveviewProtocol) lvProtocol;
|
||||
mBtAdapter = lvBtAdapter;
|
||||
mLiveviewSupport = lvSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] bytes) {
|
||||
if (null == bytes)
|
||||
return;
|
||||
LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length));
|
||||
try {
|
||||
mOutStream.write(bytes);
|
||||
mOutStream.flush();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error writing.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mIsConnected = connect();
|
||||
if (!mIsConnected) {
|
||||
setUpdateState(GBDevice.State.NOT_CONNECTED);
|
||||
return;
|
||||
}
|
||||
mQuit = false;
|
||||
|
||||
while (!mQuit) {
|
||||
LOG.info("Ready for a new message exchange.");
|
||||
|
||||
try {
|
||||
GBDeviceEvent deviceEvents[] = mLiveviewProtocol.decodeResponse(parseIncoming());
|
||||
if (deviceEvents == null) {
|
||||
LOG.info("unhandled message");
|
||||
} else {
|
||||
for (GBDeviceEvent deviceEvent : deviceEvents) {
|
||||
if (deviceEvent == null) {
|
||||
continue;
|
||||
}
|
||||
mLiveviewSupport.evaluateGBDeviceEvent(deviceEvent);
|
||||
}
|
||||
}
|
||||
} catch (SocketTimeoutException ignore) {
|
||||
LOG.debug("socket timeout, we can't help but ignore this");
|
||||
} catch (IOException e) {
|
||||
LOG.info(e.getMessage());
|
||||
mIsConnected = false;
|
||||
mBtSocket = null;
|
||||
mInStream = null;
|
||||
mOutStream = null;
|
||||
LOG.info("Bluetooth socket closed, will quit IO Thread");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mIsConnected = false;
|
||||
if (mBtSocket != null) {
|
||||
try {
|
||||
mBtSocket.close();
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage());
|
||||
}
|
||||
mBtSocket = null;
|
||||
}
|
||||
setUpdateState(GBDevice.State.NOT_CONNECTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean connect() {
|
||||
GBDevice.State originalState = gbDevice.getState();
|
||||
setUpdateState(GBDevice.State.CONNECTING);
|
||||
|
||||
try {
|
||||
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress());
|
||||
ParcelUuid uuids[] = btDevice.getUuids();
|
||||
if (uuids == null) {
|
||||
return false;
|
||||
}
|
||||
for (ParcelUuid uuid : uuids) {
|
||||
LOG.info("found service UUID " + uuid);
|
||||
}
|
||||
mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid());
|
||||
mBtSocket.connect();
|
||||
mInStream = mBtSocket.getInputStream();
|
||||
mOutStream = mBtSocket.getOutputStream();
|
||||
setUpdateState(GBDevice.State.CONNECTED);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Server socket cannot be started.");
|
||||
//LOG.error(e.getMessage());
|
||||
setUpdateState(originalState);
|
||||
mInStream = null;
|
||||
mOutStream = null;
|
||||
mBtSocket = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
write(mLiveviewProtocol.encodeSetTime());
|
||||
setUpdateState(GBDevice.State.INITIALIZED);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setUpdateState(GBDevice.State state) {
|
||||
gbDevice.setState(state);
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
private byte[] parseIncoming() throws IOException {
|
||||
ByteArrayOutputStream msgStream = new ByteArrayOutputStream();
|
||||
|
||||
boolean finished = false;
|
||||
ReaderState state = ReaderState.ID;
|
||||
byte[] incoming = new byte[1];
|
||||
|
||||
while (!finished) {
|
||||
mInStream.read(incoming);
|
||||
msgStream.write(incoming);
|
||||
|
||||
switch (state) {
|
||||
case ID:
|
||||
state = ReaderState.HEADER_LEN;
|
||||
incoming = new byte[1];
|
||||
break;
|
||||
case HEADER_LEN:
|
||||
int headerSize = 0xff & incoming[0];
|
||||
if (headerSize < 0)
|
||||
throw new IOException();
|
||||
state = ReaderState.HEADER;
|
||||
incoming = new byte[headerSize];
|
||||
break;
|
||||
case HEADER:
|
||||
int payloadSize = getLastInt(msgStream);
|
||||
if (payloadSize < 0 || payloadSize > 8000) //this will possibly be changed in the future
|
||||
throw new IOException();
|
||||
state = ReaderState.PAYLOAD;
|
||||
incoming = new byte[payloadSize];
|
||||
break;
|
||||
case PAYLOAD: //read is blocking, if we are here we have all the data
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
byte[] msgArray = msgStream.toByteArray();
|
||||
LOG.debug("received: " + GB.hexdump(msgArray, 0, msgArray.length));
|
||||
return msgArray;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration containing the possible internal status of the reader.
|
||||
*/
|
||||
private enum ReaderState {
|
||||
ID, HEADER_LEN, HEADER, PAYLOAD;
|
||||
}
|
||||
|
||||
private int getLastInt(ByteArrayOutputStream stream) {
|
||||
byte[] array = stream.toByteArray();
|
||||
ByteBuffer buffer = ByteBuffer.wrap(array, array.length - 4, 4);
|
||||
buffer.order(LiveviewConstants.BYTE_ORDER);
|
||||
return buffer.getInt();
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Calendar;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
public class LiveviewProtocol extends GBDeviceProtocol {
|
||||
|
||||
@Override
|
||||
public byte[] encodeFindDevice(boolean start) {
|
||||
return encodeVibrateRequest((short) 100, (short) 200);
|
||||
}
|
||||
|
||||
protected LiveviewProtocol(GBDevice device) {
|
||||
super(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
||||
int length = responseData.length;
|
||||
if (length < 4) {
|
||||
//empty message
|
||||
return null;
|
||||
} else {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(responseData, 0, length);
|
||||
byte msgId = buffer.get();
|
||||
buffer.get();
|
||||
int payloadLen = buffer.getInt();
|
||||
GBDeviceEventSendBytes reply = new GBDeviceEventSendBytes();
|
||||
if (payloadLen + 6 == length) {
|
||||
switch (msgId) {
|
||||
case LiveviewConstants.MSG_DEVICESTATUS:
|
||||
reply.encodedBytes = constructMessage(LiveviewConstants.MSG_DEVICESTATUS_ACK, new byte[]{LiveviewConstants.RESULT_OK});
|
||||
break;
|
||||
case LiveviewConstants.MSG_DISPLAYPANEL_ACK:
|
||||
reply.encodedBytes = encodeVibrateRequest((short) 100, (short) 200); //hack to make the notifications vibrate!
|
||||
break;
|
||||
default:
|
||||
}
|
||||
GBDeviceEventSendBytes ack = new GBDeviceEventSendBytes();
|
||||
ack.encodedBytes = constructMessage(LiveviewConstants.MSG_ACK, new byte[]{msgId});
|
||||
|
||||
return new GBDeviceEvent[]{ack, reply};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return super.decodeResponse(responseData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSetTime() {
|
||||
int time = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
|
||||
time += Calendar.getInstance().get(Calendar.ZONE_OFFSET) / 1000;
|
||||
time += Calendar.getInstance().get(Calendar.DST_OFFSET) / 1000;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(5);
|
||||
buffer.order(LiveviewConstants.BYTE_ORDER);
|
||||
buffer.putInt(time);
|
||||
buffer.put(LiveviewConstants.CLOCK_24H);
|
||||
return constructMessage(LiveviewConstants.MSG_GETTIME_RESP, buffer.array());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeNotification(NotificationSpec notificationSpec) {
|
||||
String headerText;
|
||||
// for SMS and EMAIL that came in though SMS or K9 receiver
|
||||
if (notificationSpec.sender != null) {
|
||||
headerText = notificationSpec.sender;
|
||||
} else {
|
||||
headerText = notificationSpec.title;
|
||||
}
|
||||
|
||||
String footerText = (null != notificationSpec.sourceName) ? notificationSpec.sourceName : "";
|
||||
String bodyText = (null != notificationSpec.body) ? notificationSpec.body : "";
|
||||
|
||||
byte[] headerTextArray = headerText.getBytes(LiveviewConstants.ENCODING);
|
||||
byte[] footerTextArray = footerText.getBytes(LiveviewConstants.ENCODING);
|
||||
byte[] bodyTextArray = bodyText.getBytes(LiveviewConstants.ENCODING);
|
||||
int size = 15 + headerTextArray.length + bodyTextArray.length + footerTextArray.length;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||
buffer.put((byte) 1);
|
||||
buffer.putShort((short) 0);
|
||||
buffer.putShort((short) 0);
|
||||
buffer.putShort((short) 0);
|
||||
buffer.put((byte) 80); //should alert but it doesn't make the liveview vibrate
|
||||
|
||||
buffer.put((byte) 0); //0 is for plaintext vs bitmapimage (1) strings
|
||||
buffer.putShort((short) headerTextArray.length);
|
||||
buffer.put(headerTextArray);
|
||||
buffer.putShort((short) bodyTextArray.length);
|
||||
buffer.put(bodyTextArray);
|
||||
buffer.putShort((short) footerTextArray.length);
|
||||
buffer.put(footerTextArray);
|
||||
return constructMessage(LiveviewConstants.MSG_DISPLAYPANEL, buffer.array());
|
||||
}
|
||||
|
||||
|
||||
//specific messages
|
||||
|
||||
public static byte[] constructMessage(byte messageType, byte[] payload) {
|
||||
ByteBuffer msgBuffer = ByteBuffer.allocate(payload.length + 6);
|
||||
msgBuffer.order(LiveviewConstants.BYTE_ORDER);
|
||||
msgBuffer.put(messageType);
|
||||
msgBuffer.put((byte) 4);
|
||||
msgBuffer.putInt(payload.length);
|
||||
msgBuffer.put(payload);
|
||||
return msgBuffer.array();
|
||||
}
|
||||
|
||||
public byte[] encodeVibrateRequest(short delay, short time) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.order(LiveviewConstants.BYTE_ORDER);
|
||||
buffer.putShort(delay);
|
||||
buffer.putShort(time);
|
||||
return constructMessage(LiveviewConstants.MSG_SETVIBRATE, buffer.array());
|
||||
}
|
||||
|
||||
public byte[] encodeCapabilitiesRequest() {
|
||||
byte[] version = LiveviewConstants.CLIENT_SOFTWARE_VERSION.getBytes(LiveviewConstants.ENCODING);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(version.length + 1);
|
||||
buffer.order(LiveviewConstants.BYTE_ORDER);
|
||||
buffer.put((byte) version.length);
|
||||
buffer.put(version);
|
||||
return constructMessage(LiveviewConstants.MSG_GETCAPS, buffer.array());
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
public class LiveviewSupport extends AbstractSerialDeviceSupport {
|
||||
|
||||
@Override
|
||||
public boolean connect() {
|
||||
getDeviceIOThread().start();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceProtocol createDeviceProtocol() {
|
||||
return new LiveviewProtocol(getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceIoThread createDeviceIOThread() {
|
||||
return new LiveviewIoThread(getDevice(), getContext(), getDeviceProtocol(), LiveviewSupport.this, getBluetoothAdapter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID uuid, String config) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int intensity) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized LiveviewIoThread getDeviceIOThread() {
|
||||
return (LiveviewIoThread) super.getDeviceIOThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
super.onNotification(notificationSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec musicStateSpec) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(byte type, long id) {
|
||||
//nothing to do ATM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
//nothing to do ATM
|
||||
}
|
||||
}
|
@ -41,10 +41,12 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareLength() {
|
||||
return wholeFirmwareBytes.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareVersion() {
|
||||
return (wholeFirmwareBytes[getOffsetFirmwareVersionMajor()] << 24)
|
||||
| (wholeFirmwareBytes[getOffsetFirmwareVersionMinor()] << 16)
|
||||
@ -89,9 +91,10 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isHeaderValid() {
|
||||
// TODO: not sure if this is a correct check!
|
||||
return ArrayUtils.equals(SINGLE_FW_HEADER, wholeFirmwareBytes, SINGLE_FW_HEADER_OFFSET, SINGLE_FW_HEADER_OFFSET + SINGLE_FW_HEADER.length);
|
||||
return ArrayUtils.equals(wholeFirmwareBytes, SINGLE_FW_HEADER, SINGLE_FW_HEADER_OFFSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -83,7 +83,7 @@ public class DeviceInfo extends AbstractInfo {
|
||||
}
|
||||
|
||||
public boolean supportsHeartrate() {
|
||||
return isMiliPro() || isMili1S() || (test1AHRMode && isMili1A());
|
||||
return isMili1S() || (test1AHRMode && isMili1A());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -116,10 +116,6 @@ public class DeviceInfo extends AbstractInfo {
|
||||
return hwVersion == 6;
|
||||
}
|
||||
|
||||
public boolean isMiliPro() {
|
||||
return hwVersion == 8 || (feature == 8 && appearance == 0);
|
||||
}
|
||||
|
||||
public String getHwVersion() {
|
||||
if (isMili1()) {
|
||||
return MiBandConst.MI_1;
|
||||
@ -133,9 +129,6 @@ public class DeviceInfo extends AbstractInfo {
|
||||
if (isAmazFit()) {
|
||||
return MiBandConst.MI_AMAZFIT;
|
||||
}
|
||||
if (isMiliPro()) {
|
||||
return MiBandConst.MI_PRO;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,10 @@ public class Mi1SFirmwareInfo extends CompositeMiFirmwareInfo {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isHeaderValid() {
|
||||
// TODO: not sure if this is a correct check!
|
||||
return ArrayUtils.equals(DOUBLE_FW_HEADER, wholeFirmwareBytes, DOUBLE_FW_HEADER_OFFSET, DOUBLE_FW_HEADER_OFFSET + DOUBLE_FW_HEADER.length);
|
||||
return ArrayUtils.equals(wholeFirmwareBytes, DOUBLE_FW_HEADER, DOUBLE_FW_HEADER_OFFSET);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -20,7 +20,6 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
@ -1190,6 +1189,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
// nothing yet
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
try {
|
||||
|
@ -0,0 +1,17 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||
|
||||
public abstract class AbstractMiBand1Operation extends AbstractMiBandOperation<MiBandSupport> {
|
||||
protected AbstractMiBand1Operation(MiBandSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) {
|
||||
builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
|
||||
.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
|
||||
}
|
||||
}
|
@ -4,23 +4,23 @@ import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public abstract class AbstractMiBandOperation extends AbstractBTLEOperation<MiBandSupport> {
|
||||
protected AbstractMiBandOperation(MiBandSupport support) {
|
||||
public abstract class AbstractMiBandOperation<T extends AbstractBTLEDeviceSupport> extends AbstractBTLEOperation<T> {
|
||||
protected AbstractMiBandOperation(T support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prePerform() throws IOException {
|
||||
super.prePerform();
|
||||
getDevice().setBusyTask("fetch activity data"); // mark as busy quickly to avoid interruptions from the outside
|
||||
getDevice().setBusyTask("Operation starting..."); // mark as busy quickly to avoid interruptions from the outside
|
||||
TransactionBuilder builder = performInitialized("disabling some notifications");
|
||||
enableOtherNotifications(builder, false);
|
||||
enableNeededNotifications(builder, true);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation<MiBa
|
||||
unsetBusy();
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("reenabling disabled notifications");
|
||||
enableOtherNotifications(builder, true);
|
||||
handleFinished(builder);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
GB.toast(getContext(), "Error enabling Mi Band notifications, you may need to connect and disconnect", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
@ -39,6 +39,20 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation<MiBa
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void handleFinished(TransactionBuilder builder) {
|
||||
enableNeededNotifications(builder, false);
|
||||
enableOtherNotifications(builder, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the notifications that are needed for the entire operation.
|
||||
* Enabled on operation start and disabled on operation finish.
|
||||
* @param builder
|
||||
* @param enable
|
||||
*/
|
||||
protected abstract void enableNeededNotifications(TransactionBuilder builder, boolean enable);
|
||||
|
||||
/**
|
||||
* Enables or disables certain kinds of notifications that could interfere with this
|
||||
* operation. Call this method once initially to disable other notifications, and once
|
||||
@ -47,8 +61,5 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation<MiBa
|
||||
* @param builder
|
||||
* @param enable true to enable, false to disable the other notifications
|
||||
*/
|
||||
protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) {
|
||||
builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
|
||||
.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
|
||||
}
|
||||
protected abstract void enableOtherNotifications(TransactionBuilder builder, boolean enable);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
* An operation that fetches activity data. For every fetch, a new operation must
|
||||
* be created, i.e. an operation may not be reused for multiple fetches.
|
||||
*/
|
||||
public class FetchActivityOperation extends AbstractMiBandOperation {
|
||||
public class FetchActivityOperation extends AbstractMiBand1Operation {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class);
|
||||
private static final byte[] fetch = new byte[]{MiBandService.COMMAND_FETCH_DATA};
|
||||
|
||||
@ -141,6 +141,11 @@ public class FetchActivityOperation extends AbstractMiBandOperation {
|
||||
activityStruct = new ActivityStruct(activityDataHolderSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
|
||||
// enabled all the time... maybe we should change that!
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPerform() throws IOException {
|
||||
// scheduleTaskExecutor = Executors.newScheduledThreadPool(1);
|
||||
@ -326,7 +331,7 @@ public class FetchActivityOperation extends AbstractMiBandOperation {
|
||||
steps = activityStruct.activityDataHolder[i + 2];
|
||||
if (hasExtendedActivityData) {
|
||||
heartrate = activityStruct.activityDataHolder[i + 3];
|
||||
LOG.debug("heartrate received: " + (heartrate & 0xff));
|
||||
// LOG.debug("heartrate received: " + (heartrate & 0xff));
|
||||
}
|
||||
|
||||
MiBandActivitySample sample = getSupport().createActivitySample(device, user, timestampInSeconds, provider);
|
||||
|
@ -28,7 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class UpdateFirmwareOperation extends AbstractMiBandOperation {
|
||||
public class UpdateFirmwareOperation extends AbstractMiBand1Operation {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class);
|
||||
|
||||
private final Uri uri;
|
||||
@ -41,6 +41,10 @@ public class UpdateFirmwareOperation extends AbstractMiBandOperation {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPerform() throws IOException {
|
||||
MiBandFWHelper mFwHelper = new MiBandFWHelper(uri, getContext());
|
||||
|
@ -0,0 +1,17 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.AbstractMiBandOperation;
|
||||
|
||||
public abstract class AbstractMiBand2Operation extends AbstractMiBandOperation<MiBand2Support> {
|
||||
protected AbstractMiBand2Operation(MiBand2Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) {
|
||||
// TODO: check which notifications we should disable and re-enable here
|
||||
// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
|
||||
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
|
||||
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractInfo;
|
||||
|
||||
//00000006-0000-3512-2118-0009af100700
|
||||
//
|
||||
// f = ?
|
||||
// 30 = 48%
|
||||
// 00 = 00 = STATUS_NORMAL, 01 = STATUS_CHARGING
|
||||
// e0 07 = 2016
|
||||
// 0b = 11
|
||||
// 1a = 26
|
||||
// 12 = 18
|
||||
// 23 = 35
|
||||
// 2c = 44
|
||||
// 04 = 4 // num charges??
|
||||
//
|
||||
// e0 07 = 2016 // last charge time
|
||||
// 0b = 11
|
||||
// 1a = 26
|
||||
// 17 = 23
|
||||
// 2b = 43
|
||||
// 3b = 59
|
||||
// 04 = 4 // num charges??
|
||||
// 64 = 100 // how much was charged
|
||||
|
||||
public class BatteryInfo extends AbstractInfo {
|
||||
public static final byte DEVICE_BATTERY_NORMAL = 0;
|
||||
public static final byte DEVICE_BATTERY_CHARGING = 1;
|
||||
// public static final byte DEVICE_BATTERY_LOW = 1;
|
||||
// public static final byte DEVICE_BATTERY_CHARGING_FULL = 3;
|
||||
// public static final byte DEVICE_BATTERY_CHARGE_OFF = 4;
|
||||
|
||||
public BatteryInfo(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public int getLevelInPercent() {
|
||||
if (mData.length >= 2) {
|
||||
return mData[1];
|
||||
}
|
||||
return 50; // actually unknown
|
||||
}
|
||||
|
||||
public BatteryState getState() {
|
||||
if (mData.length >= 3) {
|
||||
int value = mData[2];
|
||||
switch (value) {
|
||||
case DEVICE_BATTERY_NORMAL:
|
||||
return BatteryState.BATTERY_NORMAL;
|
||||
case DEVICE_BATTERY_CHARGING:
|
||||
return BatteryState.BATTERY_CHARGING;
|
||||
// case DEVICE_BATTERY_CHARGING:
|
||||
// return BatteryState.BATTERY_CHARGING;
|
||||
// case DEVICE_BATTERY_CHARGING_FULL:
|
||||
// return BatteryState.BATTERY_CHARGING_FULL;
|
||||
// case DEVICE_BATTERY_CHARGE_OFF:
|
||||
// return BatteryState.BATTERY_NOT_CHARGING_FULL;
|
||||
}
|
||||
}
|
||||
return BatteryState.UNKNOWN;
|
||||
}
|
||||
|
||||
public int getLastChargeLevelInParcent() {
|
||||
if (mData.length >= 20) {
|
||||
return mData[19];
|
||||
}
|
||||
return 50; // actually unknown
|
||||
}
|
||||
|
||||
public GregorianCalendar getLastChargeTime() {
|
||||
GregorianCalendar lastCharge = MiBandDateConverter.createCalendar();
|
||||
|
||||
if (mData.length >= 18) {
|
||||
lastCharge = BLETypeConversions.rawBytesToCalendar(new byte[]{
|
||||
mData[10], mData[11], mData[12], mData[13], mData[14], mData[15], mData[16], mData[17]
|
||||
}, true);
|
||||
}
|
||||
|
||||
return lastCharge;
|
||||
}
|
||||
|
||||
public int getNumCharges() {
|
||||
// if (mData.length >= 10) {
|
||||
// return ((0xff & mData[7]) | ((0xff & mData[8]) << 8));
|
||||
//
|
||||
// }
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||
|
||||
public class Mi2FirmwareInfo {
|
||||
private static final byte[] FW_HEADER = new byte[]{
|
||||
(byte) 0xa3,
|
||||
(byte) 0x68,
|
||||
(byte) 0x04,
|
||||
(byte) 0x3b,
|
||||
(byte) 0x02,
|
||||
(byte) 0xdb,
|
||||
(byte) 0xc8,
|
||||
(byte) 0x58,
|
||||
(byte) 0xd0,
|
||||
(byte) 0x50,
|
||||
(byte) 0xfa,
|
||||
(byte) 0xe7,
|
||||
(byte) 0x0c,
|
||||
(byte) 0x34,
|
||||
(byte) 0xf3,
|
||||
(byte) 0xe7,
|
||||
};
|
||||
private static final int FW_HEADER_OFFSET = 0x150;
|
||||
|
||||
private static Map<Integer,String> crcToVersion = new HashMap<>();
|
||||
static {
|
||||
crcToVersion.put(41899, "1.0.0.39");
|
||||
}
|
||||
|
||||
public static String toVersion(int crc16) {
|
||||
return crcToVersion.get(crc16);
|
||||
}
|
||||
|
||||
public static int[] getWhitelistedVersions() {
|
||||
return ArrayUtils.toIntArray(crcToVersion.keySet());
|
||||
}
|
||||
|
||||
private final int crc16;
|
||||
|
||||
private byte[] bytes;
|
||||
private String firmwareVersion;
|
||||
|
||||
public Mi2FirmwareInfo(byte[] bytes) {
|
||||
this.bytes = bytes;
|
||||
crc16 = CheckSums.getCRC16(bytes);
|
||||
firmwareVersion = crcToVersion.get(crc16);
|
||||
}
|
||||
|
||||
public boolean isGenerallyCompatibleWith(GBDevice device) {
|
||||
return isHeaderValid() && device.getType() == DeviceType.MIBAND2;
|
||||
}
|
||||
|
||||
public boolean isHeaderValid() {
|
||||
// TODO: this is certainly not a correct validation, but it works for now
|
||||
return ArrayUtils.equals(bytes, FW_HEADER, FW_HEADER_OFFSET);
|
||||
}
|
||||
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the firmware in number of bytes.
|
||||
* @return
|
||||
*/
|
||||
public int getSize() {
|
||||
return bytes.length;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getCrc16() {
|
||||
return crc16;
|
||||
}
|
||||
|
||||
public int getFirmwareVersion() {
|
||||
return getCrc16(); // HACK until we know how to determine the version from the fw bytes
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.V2NotificationStrategy;
|
||||
|
||||
public class Mi2NotificationStrategy extends V2NotificationStrategy {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
@ -10,6 +10,7 @@ import android.net.Uri;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -17,25 +18,35 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents;
|
||||
@ -53,13 +64,18 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWriteAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2NotificationStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.CheckAuthenticationNeededAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.DeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
@ -106,6 +122,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||
private RealtimeSamplesSupport realtimeSamplesSupport;
|
||||
|
||||
public MiBand2Support() {
|
||||
super(LOG);
|
||||
@ -118,10 +135,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE);
|
||||
addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
|
||||
addSupportedService(MiBand2Service.UUID_SERVICE_FIRMWARE_SERVICE);
|
||||
|
||||
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||
addSupportedProfile(deviceInfoProfile);
|
||||
heartRateProfile = new HeartRateProfile<MiBand2Support>(this);
|
||||
heartRateProfile = new HeartRateProfile<>(this);
|
||||
addSupportedProfile(heartRateProfile);
|
||||
|
||||
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
|
||||
@ -170,19 +188,31 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
return builder;
|
||||
}
|
||||
|
||||
// private MiBand2Support maybeAuth(TransactionBuilder builder) {
|
||||
// builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x20, 0x00});
|
||||
// builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x03,0x00,(byte)0x8e,(byte)0xce,0x5a,0x09,(byte)0xb3,(byte)0xd8,0x55,0x57,0x10,0x2a,(byte)0xed,0x7d,0x6b,0x78,(byte)0xc5,(byte)0xd2});
|
||||
// return this;
|
||||
// }
|
||||
public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) {
|
||||
byte[] bytes;
|
||||
if (precision == TimeUnit.MINUTES) {
|
||||
bytes = BLETypeConversions.shortCalendarToRawBytes(calendar, true);
|
||||
} else if (precision == TimeUnit.SECONDS) {
|
||||
bytes = BLETypeConversions.calendarToRawBytes(calendar, true);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported precision, only MINUTES and SECONDS are supported till now");
|
||||
}
|
||||
byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone
|
||||
// byte[] tail = new byte[] { 0x2 }; // reason
|
||||
byte[] all = BLETypeConversions.join(bytes, tail);
|
||||
return all;
|
||||
}
|
||||
|
||||
public Calendar fromTimeBytes(byte[] bytes) {
|
||||
GregorianCalendar timestamp = BLETypeConversions.rawBytesToCalendar(bytes, true);
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) {
|
||||
GregorianCalendar now = BLETypeConversions.createCalendar();
|
||||
byte[] bytes = BLETypeConversions.calendarToRawBytes(now, true);
|
||||
byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(now.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone
|
||||
// byte[] tail = new byte[] { 0x2 }; // reason
|
||||
byte[] all = BLETypeConversions.join(bytes, tail);
|
||||
builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), all);
|
||||
byte[] bytes = getTimeBytes(now, TimeUnit.SECONDS);
|
||||
builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes);
|
||||
|
||||
// byte[] localtime = BLETypeConversions.calendarToLocalTimeBytes(now);
|
||||
// builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION), localtime);
|
||||
// builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), new byte[] {0x2, 0x00});
|
||||
@ -232,18 +262,15 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
builder.notify(getCharacteristic(GattService.UUID_SERVICE_CURRENT_TIME), enable);
|
||||
// Notify CHARACTERISTIC9 to receive random auth code
|
||||
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), enable);
|
||||
builder.notify(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), enable);
|
||||
builder.notify(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4), enable);
|
||||
builder.notify(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT), enable);
|
||||
return this;
|
||||
}
|
||||
|
||||
private MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) {
|
||||
builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
|
||||
.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable)
|
||||
.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable)
|
||||
.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
|
||||
// cannot use supportsHeartrate() here because we don't have that information yet
|
||||
public MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) {
|
||||
// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable)
|
||||
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable)
|
||||
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable);
|
||||
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), enable);
|
||||
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable);
|
||||
BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT);
|
||||
if (heartrateCharacteristic != null) {
|
||||
builder.notify(heartrateCharacteristic, enable);
|
||||
@ -340,6 +367,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
return this;
|
||||
}
|
||||
|
||||
private MiBand2Support requestBatteryInfo(TransactionBuilder builder) {
|
||||
LOG.debug("Requesting Battery Info!");
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO);
|
||||
builder.read(characteristic);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MiBand2Support requestDeviceInfo(TransactionBuilder builder) {
|
||||
LOG.debug("Requesting Device Info!");
|
||||
deviceInfoProfile.requestDeviceInfo(builder);
|
||||
@ -380,15 +414,15 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
private MiBand2Support setFitnessGoal(TransactionBuilder transaction) {
|
||||
LOG.info("Attempting to set Fitness Goal...");
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT);
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8);
|
||||
if (characteristic != null) {
|
||||
int fitnessGoal = MiBandCoordinator.getFitnessGoal(getDevice().getAddress());
|
||||
transaction.write(characteristic, new byte[]{
|
||||
MiBandService.COMMAND_SET_FITNESS_GOAL,
|
||||
0,
|
||||
(byte) (fitnessGoal & 0xff),
|
||||
(byte) ((fitnessGoal >>> 8) & 0xff)
|
||||
});
|
||||
byte[] bytes = ArrayUtils.addAll(
|
||||
MiBand2Service.COMMAND_SET_FITNESS_GOAL_START,
|
||||
BLETypeConversions.fromUint16(fitnessGoal));
|
||||
bytes = ArrayUtils.addAll(bytes,
|
||||
MiBand2Service.COMMAND_SET_FITNESS_GOAL_END);
|
||||
transaction.write(characteristic, bytes);
|
||||
} else {
|
||||
LOG.info("Unable to set Fitness Goal");
|
||||
}
|
||||
@ -398,28 +432,24 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
/**
|
||||
* Part of device initialization process. Do not call manually.
|
||||
*
|
||||
* @param transaction
|
||||
* @param builder
|
||||
* @return
|
||||
*/
|
||||
private MiBand2Support setWearLocation(TransactionBuilder transaction) {
|
||||
private MiBand2Support setWearLocation(TransactionBuilder builder) {
|
||||
LOG.info("Attempting to set wear location...");
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT);
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8);
|
||||
if (characteristic != null) {
|
||||
transaction.add(new ConditionalWriteAction(characteristic) {
|
||||
@Override
|
||||
protected byte[] checkCondition() {
|
||||
if (getDeviceInfo() != null && getDeviceInfo().isAmazFit()) {
|
||||
return null;
|
||||
}
|
||||
int location = MiBandCoordinator.getWearLocation(getDevice().getAddress());
|
||||
return new byte[]{
|
||||
MiBandService.COMMAND_SET_WEAR_LOCATION,
|
||||
(byte) location
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOG.info("Unable to set Wear Location");
|
||||
builder.notify(characteristic, true);
|
||||
int location = MiBandCoordinator.getWearLocation(getDevice().getAddress());
|
||||
switch (location) {
|
||||
case 0: // left hand
|
||||
builder.write(characteristic, MiBand2Service.WEAR_LOCATION_LEFT_WRIST);
|
||||
break;
|
||||
case 1: // right hand
|
||||
builder.write(characteristic, MiBand2Service.WEAR_LOCATION_RIGHT_WRIST);
|
||||
break;
|
||||
}
|
||||
builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed.
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@ -451,23 +481,18 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
* @param builder
|
||||
*/
|
||||
private MiBand2Support setHeartrateSleepSupport(TransactionBuilder builder) {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT);
|
||||
if (characteristic != null) {
|
||||
builder.add(new ConditionalWriteAction(characteristic) {
|
||||
@Override
|
||||
protected byte[] checkCondition() {
|
||||
if (!supportsHeartRate()) {
|
||||
return null;
|
||||
}
|
||||
if (MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress())) {
|
||||
LOG.info("Enabling heartrate sleep support...");
|
||||
return startHeartMeasurementSleep;
|
||||
} else {
|
||||
LOG.info("Disabling heartrate sleep support...");
|
||||
return stopHeartMeasurementSleep;
|
||||
}
|
||||
}
|
||||
});
|
||||
BluetoothGattCharacteristic characteristicHRControlPoint = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT);
|
||||
final boolean enableHrSleepSupport = MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress());
|
||||
if (characteristicHRControlPoint != null) {
|
||||
builder.notify(characteristicHRControlPoint, true);
|
||||
if (enableHrSleepSupport) {
|
||||
LOG.info("Enabling heartrate sleep support...");
|
||||
builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_ENABLE_HR_SLEEP_MEASUREMENT);
|
||||
} else {
|
||||
LOG.info("Disabling heartrate sleep support...");
|
||||
builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT);
|
||||
}
|
||||
builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed.
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@ -541,7 +566,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
try {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3);
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION);
|
||||
TransactionBuilder builder = performInitialized("Set alarm");
|
||||
boolean anyAlarmEnabled = false;
|
||||
for (Alarm alarm : alarms) {
|
||||
@ -573,46 +598,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
public void onSetTime() {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("Set date and time");
|
||||
setCurrentTime(builder);
|
||||
setCurrentTimeWithService(builder);
|
||||
//TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm.
|
||||
sendCalendarEvents(builder);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to set time on MI device", ex);
|
||||
}
|
||||
//TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm.
|
||||
sendCalendarEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current time to the Mi device using the given builder.
|
||||
*
|
||||
* @param builder
|
||||
*/
|
||||
private MiBand2Support setCurrentTime(TransactionBuilder builder) {
|
||||
Calendar now = GregorianCalendar.getInstance();
|
||||
Date date = now.getTime();
|
||||
LOG.info("Sending current time to Mi Band: " + DateTimeUtils.formatDate(date) + " (" + date.toGMTString() + ")");
|
||||
byte[] nowBytes = MiBandDateConverter.calendarToRawBytes(now);
|
||||
byte[] time = new byte[]{
|
||||
nowBytes[0],
|
||||
nowBytes[1],
|
||||
nowBytes[2],
|
||||
nowBytes[3],
|
||||
nowBytes[4],
|
||||
nowBytes[5],
|
||||
(byte) 0x0f,
|
||||
(byte) 0x0f,
|
||||
(byte) 0x0f,
|
||||
(byte) 0x0f,
|
||||
(byte) 0x0f,
|
||||
(byte) 0x0f
|
||||
};
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME);
|
||||
if (characteristic != null) {
|
||||
builder.write(characteristic, time);
|
||||
} else {
|
||||
LOG.info("Unable to set time -- characteristic not available");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -675,45 +667,34 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
if (supportsHeartRate()) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("HeartRateTest");
|
||||
heartRateProfile.requestHeartRateMeasurement(builder);
|
||||
// profile.resetEnergyExpended(builder);
|
||||
// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
|
||||
// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual);
|
||||
// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to read HearRate with MI2", ex);
|
||||
}
|
||||
} else {
|
||||
GB.toast(getContext(), "Heart rate is not supported on this device", Toast.LENGTH_LONG, GB.ERROR);
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("HeartRateTest");
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual);
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to read HearRate with MI2", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
if (supportsHeartRate()) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("EnableRealtimeHeartRateMeasurement");
|
||||
if (enable) {
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual);
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous);
|
||||
} else {
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex);
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("Enable realtime heart rateM measurement");
|
||||
if (enable) {
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual);
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous);
|
||||
} else {
|
||||
builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous);
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
enableRealtimeSamplesTimer(enable);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean supportsHeartRate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
isLocatingDevice = start;
|
||||
@ -736,28 +717,28 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onFetchActivityData() {
|
||||
// TODO: onFetchActivityData
|
||||
// try {
|
||||
// new FetchActivityOperation(this).perform();
|
||||
// } catch (IOException ex) {
|
||||
// LOG.error("Unable to fetch MI activity data", ex);
|
||||
// }
|
||||
try {
|
||||
new FetchActivityOperation(this).perform();
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to fetch MI activity data", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(boolean enable) {
|
||||
try {
|
||||
BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT);
|
||||
if (enable) {
|
||||
TransactionBuilder builder = performInitialized("Read realtime steps");
|
||||
builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS)).queue(getQueue());
|
||||
}
|
||||
performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications")
|
||||
.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency())
|
||||
.write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to change realtime steps notification to: " + enable, e);
|
||||
}
|
||||
// try {
|
||||
// BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT);
|
||||
// if (enable) {
|
||||
// TransactionBuilder builder = performInitialized("Read realtime steps");
|
||||
// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS)).queue(getQueue());
|
||||
// }
|
||||
// performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications")
|
||||
// .write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency())
|
||||
// .write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue());
|
||||
// enableRealtimeSamplesTimer(enable);
|
||||
// } catch (IOException e) {
|
||||
// LOG.error("Unable to change realtime steps notification to: " + enable, e);
|
||||
// }
|
||||
}
|
||||
|
||||
private byte[] getHighLatency() {
|
||||
@ -800,12 +781,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
// TODO: onInstallApp (firmware update)
|
||||
// try {
|
||||
// new UpdateFirmwareOperation(uri, this).perform();
|
||||
// } catch (IOException ex) {
|
||||
// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
// }
|
||||
try {
|
||||
new UpdateFirmwareOperation(uri, this).perform();
|
||||
} catch (IOException ex) {
|
||||
GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -844,7 +824,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
super.onCharacteristicChanged(gatt, characteristic);
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
|
||||
if (MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) {
|
||||
handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS);
|
||||
return true;
|
||||
} else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) {
|
||||
@ -880,13 +860,10 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
super.onCharacteristicRead(gatt, characteristic, status);
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
if (MiBandService.UUID_CHARACTERISTIC_DEVICE_INFO.equals(characteristicUUID)) {
|
||||
handleDeviceInfo(characteristic.getValue(), status);
|
||||
return true;
|
||||
} else if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) {
|
||||
if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) {
|
||||
handleDeviceName(characteristic.getValue(), status);
|
||||
return true;
|
||||
} else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) {
|
||||
} else if (MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) {
|
||||
handleBatteryInfo(characteristic.getValue(), status);
|
||||
return true;
|
||||
} else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) {
|
||||
@ -935,7 +912,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
public void logHeartrate(byte[] value, int status) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS && value != null) {
|
||||
LOG.info("Got heartrate:");
|
||||
if (value.length == 2 && value[0] == 6) {
|
||||
if (value.length == 2 && value[0] == 0) {
|
||||
int hrValue = (value[1] & 0xff);
|
||||
GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO);
|
||||
}
|
||||
@ -945,15 +922,17 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
private void handleHeartrate(byte[] value) {
|
||||
if (value.length == 2 && value[0] == 6) {
|
||||
if (value.length == 2 && value[0] == 0) {
|
||||
int hrValue = (value[1] & 0xff);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("heart rate: " + hrValue);
|
||||
}
|
||||
Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT)
|
||||
.putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, hrValue)
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport();
|
||||
realtimeSamplesSupport.setHeartrateBpm(hrValue);
|
||||
if (!realtimeSamplesSupport.isRunning()) {
|
||||
// single shot measurement, manually invoke storage and result publishing
|
||||
realtimeSamplesSupport.triggerCurrentSample();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -962,10 +941,75 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("realtime steps: " + steps);
|
||||
}
|
||||
Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS)
|
||||
.putExtra(DeviceService.EXTRA_REALTIME_STEPS, steps)
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
getRealtimeSamplesSupport().setSteps(steps);
|
||||
}
|
||||
|
||||
private void enableRealtimeSamplesTimer(boolean enable) {
|
||||
if (enable) {
|
||||
getRealtimeSamplesSupport().start();
|
||||
} else {
|
||||
if (realtimeSamplesSupport != null) {
|
||||
realtimeSamplesSupport.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) {
|
||||
MiBandActivitySample sample = new MiBandActivitySample();
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
sample.setTimestamp(timestampInSeconds);
|
||||
sample.setProvider(provider);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
private RealtimeSamplesSupport getRealtimeSamplesSupport() {
|
||||
if (realtimeSamplesSupport == null) {
|
||||
realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) {
|
||||
@Override
|
||||
public void doCurrentSample() {
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
DaoSession session = handler.getDaoSession();
|
||||
|
||||
Device device = DBHelper.getDevice(getDevice(), session);
|
||||
User user = DBHelper.getUser(session);
|
||||
int ts = (int) (System.currentTimeMillis() / 1000);
|
||||
MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session);
|
||||
MiBandActivitySample sample = createActivitySample(device, user, ts, provider);
|
||||
sample.setHeartRate(getHeartrateBpm());
|
||||
sample.setSteps(getSteps());
|
||||
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
||||
sample.setRawKind(MiBand2SampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?
|
||||
|
||||
// TODO: remove this once fully ported to REALTIME_SAMPLES
|
||||
if (sample.getSteps() != ActivitySample.NOT_MEASURED) {
|
||||
Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS)
|
||||
.putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps())
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
if (sample.getHeartRate() != ActivitySample.NOT_MEASURED) {
|
||||
Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT)
|
||||
.putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, sample.getHeartRate())
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
// Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
|
||||
// .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
|
||||
// LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
|
||||
LOG.debug("Storing realtime sample: " + sample);
|
||||
provider.addGBActivitySample(sample);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Unable to acquire db for saving realtime samples", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return realtimeSamplesSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1028,19 +1072,6 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeviceInfo(byte[] value, int status) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
mDeviceInfo = new DeviceInfo(value);
|
||||
if (getDeviceInfo().supportsHeartrate()) {
|
||||
getDevice().setFirmwareVersion2(MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion()));
|
||||
}
|
||||
LOG.warn("Device info: " + mDeviceInfo);
|
||||
versionCmd.hwVersion = mDeviceInfo.getHwVersion();
|
||||
versionCmd.fwVersion = MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getFirmwareVersion());
|
||||
handleGBDeviceEvent(versionCmd);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeviceName(byte[] value, int status) {
|
||||
// if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
// versionCmd.hwVersion = new String(value);
|
||||
@ -1165,39 +1196,94 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
/**
|
||||
* Fetch the events from the android device calendars and set the alarms on the miband.
|
||||
* @param builder
|
||||
*/
|
||||
private void sendCalendarEvents() {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("Send upcoming events");
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3);
|
||||
private MiBand2Support sendCalendarEvents(TransactionBuilder builder) {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION);
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
|
||||
|
||||
if (availableSlots > 0) {
|
||||
CalendarEvents upcomingEvents = new CalendarEvents();
|
||||
List<CalendarEvents.CalendarEvent> mEvents = upcomingEvents.getCalendarEventList(getContext());
|
||||
if (availableSlots > 0) {
|
||||
CalendarEvents upcomingEvents = new CalendarEvents();
|
||||
List<CalendarEvents.CalendarEvent> mEvents = upcomingEvents.getCalendarEventList(getContext());
|
||||
|
||||
int iteration = 0;
|
||||
for (CalendarEvents.CalendarEvent mEvt : mEvents) {
|
||||
if (iteration >= availableSlots || iteration > 2) {
|
||||
break;
|
||||
}
|
||||
int slotToUse = 2 - iteration;
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(mEvt.getBegin());
|
||||
Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar);
|
||||
queueAlarm(alarm, builder, characteristic);
|
||||
iteration++;
|
||||
int iteration = 0;
|
||||
for (CalendarEvents.CalendarEvent mEvt : mEvents) {
|
||||
if (iteration >= availableSlots || iteration > 2) {
|
||||
break;
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
int slotToUse = 2 - iteration;
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(mEvt.getBegin());
|
||||
Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar);
|
||||
queueAlarm(alarm, builder, characteristic);
|
||||
iteration++;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to send Events to MI device", ex);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
TransactionBuilder builder = null;
|
||||
try {
|
||||
builder = performInitialized("Sending configuration for option: " + config);
|
||||
switch (config) {
|
||||
case MiBandConst.PREF_MI2_DATEFORMAT:
|
||||
setDateDisplay(builder);
|
||||
break;
|
||||
case MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT:
|
||||
setActivateDisplayOnLiftWrist(builder);
|
||||
break;
|
||||
case MiBandConst.PREF_MIBAND_FITNESS_GOAL:
|
||||
setFitnessGoal(builder);
|
||||
break;
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
}
|
||||
|
||||
private MiBand2Support setDateDisplay(TransactionBuilder builder) {
|
||||
DateTimeDisplay dateTimeDisplay = MiBand2Coordinator.getDateDisplay(getContext());
|
||||
LOG.info("Setting date display to " + dateTimeDisplay);
|
||||
switch (dateTimeDisplay) {
|
||||
case TIME:
|
||||
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME);
|
||||
break;
|
||||
case DATE_TIME:
|
||||
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_DATE_TIME);
|
||||
break;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) {
|
||||
boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist();
|
||||
LOG.info("Setting activate display on lift wrist to " + enable);
|
||||
if (enable) {
|
||||
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST);
|
||||
} else {
|
||||
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void phase2Initialize(TransactionBuilder builder) {
|
||||
LOG.info("phase2Initialize...");
|
||||
enableFurtherNotifications(builder, true);
|
||||
requestBatteryInfo(builder);
|
||||
setDateDisplay(builder);
|
||||
setWearLocation(builder);
|
||||
setFitnessGoal(builder);
|
||||
setActivateDisplayOnLiftWrist(builder);
|
||||
setHeartrateSleepSupport(builder);
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* An operation that fetches activity data. For every fetch, a new operation must
|
||||
* be created, i.e. an operation may not be reused for multiple fetches.
|
||||
*/
|
||||
public class FetchActivityOperation extends AbstractMiBand2Operation {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class);
|
||||
|
||||
private List<MiBandActivitySample> samples = new ArrayList<>(60*24); // 1day per default
|
||||
|
||||
private byte lastPacketCounter = -1;
|
||||
private Calendar startTimestamp;
|
||||
|
||||
public FetchActivityOperation(MiBand2Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
|
||||
if (!enable) {
|
||||
// dynamically enabled, but always disabled on finish
|
||||
builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA), enable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPerform() throws IOException {
|
||||
TransactionBuilder builder = performInitialized("fetching activity data");
|
||||
getSupport().setLowLatency(builder);
|
||||
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
|
||||
BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4);
|
||||
builder.notify(characteristicFetch, true);
|
||||
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
|
||||
|
||||
GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime();
|
||||
builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)));
|
||||
builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply
|
||||
builder.notify(characteristicActivityData, true);
|
||||
builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_ACTIVITY_DATA });
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
private GregorianCalendar getLastSuccessfulSynchronizedTime() {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
SampleProvider<MiBandActivitySample> sampleProvider = new MiBand2SampleProvider(getDevice(), session);
|
||||
MiBandActivitySample sample = sampleProvider.getLatestActivitySample();
|
||||
if (sample != null) {
|
||||
int timestamp = sample.getTimestamp();
|
||||
GregorianCalendar calendar = BLETypeConversions.createCalendar();
|
||||
calendar.setTimeInMillis((long) timestamp * 1000);
|
||||
return calendar;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error querying for latest activity sample, synchronizing the last 10 days", ex);
|
||||
}
|
||||
|
||||
GregorianCalendar calendar = BLETypeConversions.createCalendar();
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -10);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
if (MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) {
|
||||
handleActivityNotif(characteristic.getValue());
|
||||
return true;
|
||||
} else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
|
||||
handleActivityMetadata(characteristic.getValue());
|
||||
return true;
|
||||
} else {
|
||||
return super.onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleActivityFetchFinish() {
|
||||
LOG.info("Fetching activity data has finished.");
|
||||
saveSamples();
|
||||
operationFinished();
|
||||
unsetBusy();
|
||||
}
|
||||
|
||||
private void saveSamples() {
|
||||
if (samples.size() > 0) {
|
||||
// save all the samples that we got
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
DaoSession session = handler.getDaoSession();
|
||||
SampleProvider<MiBandActivitySample> sampleProvider = new MiBandSampleProvider(getDevice(), session);
|
||||
Device device = DBHelper.getDevice(getDevice(), session);
|
||||
User user = DBHelper.getUser(session);
|
||||
|
||||
GregorianCalendar timestamp = (GregorianCalendar) startTimestamp.clone();
|
||||
for (MiBandActivitySample sample : samples) {
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000));
|
||||
sample.setProvider(sampleProvider);
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
// LOG.debug("sample: " + sample);
|
||||
}
|
||||
|
||||
timestamp.add(Calendar.MINUTE, 1);
|
||||
}
|
||||
sampleProvider.addGBActivitySamples(samples.toArray(new MiBandActivitySample[0]));
|
||||
|
||||
LOG.info("Mi2 activity data: last sample timestamp: " + DateTimeUtils.formatDateTime(timestamp.getTime()));
|
||||
|
||||
} catch (Exception ex) {
|
||||
GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR);
|
||||
} finally {
|
||||
samples.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to handle the incoming activity data.
|
||||
* There are two kind of messages we currently know:
|
||||
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
|
||||
* - the second one is 20 bytes long and contains the actual activity data
|
||||
* <p/>
|
||||
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
private void handleActivityNotif(byte[] value) {
|
||||
if (!isOperationRunning()) {
|
||||
LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length);
|
||||
getSupport().logMessageContent(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((value.length % 4) == 1) {
|
||||
if ((byte) (lastPacketCounter + 1) == value[0] ) {
|
||||
lastPacketCounter++;
|
||||
bufferActivityData(value);
|
||||
} else {
|
||||
GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR);
|
||||
handleActivityFetchFinish();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates samples from the given 17-length array
|
||||
* @param value
|
||||
*/
|
||||
private void bufferActivityData(byte[] value) {
|
||||
int len = value.length;
|
||||
|
||||
if (len % 4 != 1) {
|
||||
throw new AssertionError("Unexpected activity array size: " + value);
|
||||
}
|
||||
|
||||
for (int i = 1; i < len; i+=4) {
|
||||
MiBandActivitySample sample = createSample(value[i], value[i + 1], value[i + 2], value[i + 3]);
|
||||
samples.add(sample);
|
||||
}
|
||||
}
|
||||
|
||||
private MiBandActivitySample createSample(byte category, byte intensity, byte steps, byte heartrate) {
|
||||
MiBandActivitySample sample = new MiBandActivitySample();
|
||||
sample.setRawKind(category & 0xff);
|
||||
sample.setRawIntensity(intensity & 0xff);
|
||||
sample.setSteps(steps & 0xff);
|
||||
sample.setHeartRate(heartrate & 0xff);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
private void handleActivityMetadata(byte[] value) {
|
||||
if (value.length == 15) {
|
||||
// first two bytes are whether our request was accepted
|
||||
if (ArrayUtils.equals(value, MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
|
||||
// the third byte (0x01 on success) = ?
|
||||
// the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect
|
||||
|
||||
// last 8 bytes are the start date
|
||||
Calendar startTimestamp = getSupport().fromTimeBytes(org.apache.commons.lang3.ArrayUtils.subarray(value, 7, value.length));
|
||||
setStartTimestamp(startTimestamp);
|
||||
|
||||
GB.toast(getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
|
||||
DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO);
|
||||
} else {
|
||||
LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value));
|
||||
handleActivityFetchFinish();
|
||||
}
|
||||
} else if (value.length == 3) {
|
||||
if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) {
|
||||
handleActivityFetchFinish();
|
||||
} else {
|
||||
LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value));
|
||||
handleActivityFetchFinish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStartTimestamp(Calendar startTimestamp) {
|
||||
this.startTimestamp = startTimestamp;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user