Merge branch 'master' into new_GUI

master
cpfeiffer 2016-12-15 20:31:04 +01:00
commit d12103e95d
151 changed files with 5158 additions and 1264 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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)
* Mi Band notifications (LEDs + vibration) for
* Discovery and pairing
* Discovery and pairing
* Mi Band notifications (LEDs + vibration) for
* 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

View File

@ -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'

View File

@ -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"

View File

@ -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

View File

@ -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');
}
});
}

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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) {

View File

@ -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);

View File

@ -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;

View File

@ -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)

View File

@ -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();
}

View File

@ -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);

View File

@ -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";

View File

@ -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

View File

@ -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

View File

@ -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;
}
}
/**

View File

@ -1,6 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.res.Resources;
import android.view.ViewGroup;
import java.util.Date;

View File

@ -32,9 +32,4 @@ public class TimestampValueFormatter implements IAxisValueFormatter {
String dateString = dateFormat.format(date);
return dateString;
}
@Override
public int getDecimalDigits() {
return 0;
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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) {
}
}

View File

@ -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) {
}
}

View File

@ -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;

View File

@ -12,12 +12,14 @@ public class GBDeviceEventAppManagement extends GBDeviceEvent {
UNKNOWN,
INSTALL,
DELETE,
START,
STOP,
}
public enum Event {
UNKNOWN,
SUCCESS,
ACKNOLEDGE,
ACKNOWLEDGE,
FAILURE,
REQUEST,
}

View File

@ -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());

View File

@ -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

View File

@ -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();
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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

View File

@ -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 &lt;xperimental@solidproject.de&gt;
*/
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";
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
public enum DateTimeDisplay {
TIME,
DATE_TIME
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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");

View File

@ -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";

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -181,8 +181,8 @@ 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;
}
}
}

View File

@ -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();
@ -342,4 +342,4 @@ public class PBWReader {
public JSONObject getAppKeysJSON() {
return mAppKeys;
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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() +
'}';

View File

@ -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();

View File

@ -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,

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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) {

View File

@ -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:

View File

@ -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

View File

@ -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);

View File

@ -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")) {

View File

@ -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
*/

View File

@ -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) {

View File

@ -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

View File

@ -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();

View File

@ -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 {

View File

@ -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);
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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),

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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 "?";
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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